From b421bc749354afa0f2a7cd89cf8103313334913a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 9 Sep 2024 14:58:25 +0200 Subject: [PATCH 01/43] API specification --- .../api/ScenarioActivityApiHttpService.scala | 134 +- .../touk/nussknacker/ui/api/TapirCodecs.scala | 13 +- .../ScenarioActivityApiEndpoints.scala | 280 --- .../description/scenarioActivity/Dtos.scala | 528 +++++ .../scenarioActivity/Endpoints.scala | 192 ++ .../scenarioActivity/Examples.scala | 236 ++ .../scenarioActivity/InputOutput.scala | 30 + ...DesignerApiAvailableToExposeYamlSpec.scala | 36 +- docs-internal/api/nu-designer-openapi.yaml | 1930 +++++++++++++++-- 9 files changed, 2897 insertions(+), 482 deletions(-) delete mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ScenarioActivityApiEndpoints.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Dtos.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Endpoints.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Examples.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/InputOutput.scala 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 8a9e1112a0b..486ad53dd3b 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 @@ -5,13 +5,13 @@ import com.typesafe.scalalogging.LazyLogging 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.ScenarioActivityApiEndpoints -import pl.touk.nussknacker.ui.api.description.ScenarioActivityApiEndpoints.Dtos.ScenarioActivityError.{ +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError.{ NoComment, NoPermission, NoScenario } -import pl.touk.nussknacker.ui.api.description.ScenarioActivityApiEndpoints.Dtos._ +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.process.{ProcessService, ScenarioAttachmentService} import pl.touk.nussknacker.ui.security.api.{AuthManager, LoggedUser} @@ -29,29 +29,48 @@ class ScenarioActivityApiHttpService( scenarioService: ProcessService, scenarioAuthorizer: AuthorizeProcess, attachmentService: ScenarioAttachmentService, - streamEndpointProvider: TapirStreamEndpointProvider + streamEndpointProvider: TapirStreamEndpointProvider, )(implicit executionContext: ExecutionContext) extends BaseHttpService(authManager) with LazyLogging { - private val scenarioActivityApiEndpoints = new ScenarioActivityApiEndpoints( - authManager.authenticationEndpointInput() - ) + private val securityInput = authManager.authenticationEndpointInput() + + private val endpoints = new Endpoints(securityInput, streamEndpointProvider) expose { - scenarioActivityApiEndpoints.scenarioActivityEndpoint + endpoints.deprecatedScenarioActivityEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => scenarioName: ProcessName => for { - scenarioId <- getScenarioIdByName(scenarioName) - _ <- isAuthorized(scenarioId, Permission.Read) - scenarioActivity <- EitherT.right(scenarioActivityRepository.findActivity(scenarioId)) - } yield ScenarioActivity(scenarioActivity) + 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 + ) + } + ) } } expose { - scenarioActivityApiEndpoints.addCommentEndpoint + endpoints.deprecatedAddCommentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => request: AddCommentRequest => for { @@ -63,9 +82,9 @@ class ScenarioActivityApiHttpService( } expose { - scenarioActivityApiEndpoints.deleteCommentEndpoint + endpoints.deprecatedDeleteCommentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) - .serverLogicEitherT { implicit loggedUser => request: DeleteCommentRequest => + .serverLogicEitherT { implicit loggedUser => request: DeprecatedDeleteCommentRequest => for { scenarioId <- getScenarioIdByName(request.scenarioName) _ <- isAuthorized(scenarioId, Permission.Write) @@ -75,8 +94,79 @@ class ScenarioActivityApiHttpService( } expose { - scenarioActivityApiEndpoints - .addAttachmentEndpoint(streamEndpointProvider.streamBodyEndpointInput) + endpoints.scenarioActivitiesEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => scenarioName: ProcessName => + for { + scenarioId <- getScenarioIdByName(scenarioName) + _ <- isAuthorized(scenarioId, Permission.Read) + activities <- EitherT.liftF(Future.failed(new Exception("API not yet implemented"))) + } yield ScenarioActivities(activities) + } + } + + expose { + endpoints.scenarioActivitiesMetadataEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => scenarioName: ProcessName => + for { + scenarioId <- getScenarioIdByName(scenarioName) + _ <- isAuthorized(scenarioId, Permission.Read) + metadata = ScenarioActivitiesMetadata.default + } yield metadata + } + } + + expose { + endpoints.addCommentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: AddCommentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Write) + _ <- addNewComment(request, scenarioId) + } yield () + } + } + + expose { + endpoints.editCommentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: EditCommentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Write) + _ <- notImplemented[Unit] + } yield () + } + } + + expose { + endpoints.deleteCommentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: DeleteCommentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Write) + _ <- notImplemented[Unit] + } yield () + } + } + + expose { + endpoints.attachmentsEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => processName: ProcessName => + for { + scenarioId <- getScenarioIdByName(processName) + _ <- isAuthorized(scenarioId, Permission.Read) + attachments <- notImplemented[ScenarioAttachments] + } yield attachments + } + } + + expose { + endpoints.addAttachmentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => request: AddAttachmentRequest => for { @@ -88,8 +178,7 @@ class ScenarioActivityApiHttpService( } expose { - scenarioActivityApiEndpoints - .downloadAttachmentEndpoint(streamEndpointProvider.streamBodyEndpointOutput) + endpoints.downloadAttachmentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => request: GetAttachmentRequest => for { @@ -101,6 +190,9 @@ class ScenarioActivityApiHttpService( } } + 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), @@ -127,10 +219,10 @@ class ScenarioActivityApiHttpService( scenarioActivityRepository.addComment(scenarioId, request.versionId, UserComment(request.commentContent)) ) - private def deleteComment(request: DeleteCommentRequest): EitherT[Future, ScenarioActivityError, Unit] = + private def deleteComment(request: DeprecatedDeleteCommentRequest): EitherT[Future, ScenarioActivityError, Unit] = EitherT( scenarioActivityRepository.deleteComment(request.commentId) - ).leftMap(_ => NoComment(request.commentId)) + ).leftMap(_ => NoComment(request.commentId.toString)) private def saveAttachment(request: AddAttachmentRequest, scenarioId: ProcessId)( implicit loggedUser: LoggedUser diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TapirCodecs.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TapirCodecs.scala index 5cf51602f58..52fbab1403d 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TapirCodecs.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TapirCodecs.scala @@ -9,7 +9,7 @@ import pl.touk.nussknacker.engine.deployment.EngineSetupName import pl.touk.nussknacker.ui.server.HeadersSupport.{ContentDisposition, FileName} import sttp.tapir.Codec.PlainCodec import sttp.tapir.CodecFormat.TextPlain -import sttp.tapir.{Codec, CodecFormat, DecodeResult, Schema} +import sttp.tapir.{Codec, CodecFormat, DecodeResult, Schema, Validator} import java.net.URL @@ -133,4 +133,15 @@ object TapirCodecs { implicit val classSchema: Schema[Class[_]] = Schema.string[Class[_]] } + def enumSchema[T]( + items: List[T], + encoder: T => String, + ): Schema[T] = + Schema.string.validate( + Validator.enumeration( + items, + (i: T) => Some(encoder(i)), + ), + ) + } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ScenarioActivityApiEndpoints.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ScenarioActivityApiEndpoints.scala deleted file mode 100644 index db36ba1b867..00000000000 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ScenarioActivityApiEndpoints.scala +++ /dev/null @@ -1,280 +0,0 @@ -package pl.touk.nussknacker.ui.api.description - -import derevo.circe.{decoder, encoder} -import derevo.derive -import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} -import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions -import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions.SecuredEndpoint -import pl.touk.nussknacker.security.AuthCredentials -import pl.touk.nussknacker.ui.api.BaseHttpService.CustomAuthorizationError -import pl.touk.nussknacker.ui.api.TapirCodecs -import pl.touk.nussknacker.ui.process.repository.DbProcessActivityRepository.{ - Attachment => DbAttachment, - Comment => DbComment, - ProcessActivity => DbProcessActivity -} -import pl.touk.nussknacker.ui.server.HeadersSupport.FileName -import sttp.model.StatusCode.{InternalServerError, NotFound, Ok} -import sttp.model.{HeaderNames, MediaType} -import sttp.tapir.EndpointIO.Example -import sttp.tapir._ -import sttp.tapir.derevo.schema -import sttp.tapir.json.circe.jsonBody - -import java.io.InputStream -import java.time.Instant - -class ScenarioActivityApiEndpoints(auth: EndpointInput[AuthCredentials]) extends BaseEndpointDefinitions { - - import ScenarioActivityApiEndpoints.Dtos.ScenarioActivityError._ - import ScenarioActivityApiEndpoints.Dtos._ - import TapirCodecs.ContentDispositionCodec._ - import TapirCodecs.HeaderCodec._ - import TapirCodecs.ScenarioNameCodec._ - import TapirCodecs.VersionIdCodec._ - - lazy val scenarioActivityEndpoint: SecuredEndpoint[ProcessName, ScenarioActivityError, ScenarioActivity, Any] = - baseNuApiEndpoint - .summary("Scenario activity service") - .tag("Scenario") - .get - .in("processes" / path[ProcessName]("scenarioName") / "activity") - .out( - statusCode(Ok).and( - jsonBody[ScenarioActivity].example( - Example.of( - summary = Some("Display scenario activity"), - value = ScenarioActivity( - comments = List( - Comment( - id = 1L, - processVersionId = 1L, - content = "some comment", - user = "test", - createDate = Instant.parse("2024-01-17T14:21:17Z") - ) - ), - attachments = List( - Attachment( - id = 1L, - processVersionId = 1L, - fileName = "some_file.txt", - user = "test", - createDate = Instant.parse("2024-01-17T14:21:17Z") - ) - ) - ) - ) - ) - ) - ) - .errorOut(scenarioNotFoundErrorOutput) - .withSecurity(auth) - - lazy val addCommentEndpoint: SecuredEndpoint[AddCommentRequest, ScenarioActivityError, Unit, Any] = - baseNuApiEndpoint - .summary("Add scenario comment service") - .tag("Scenario") - .post - .in( - ("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" - / "comments" / stringBody).mapTo[AddCommentRequest] - ) - .out(statusCode(Ok)) - .errorOut(scenarioNotFoundErrorOutput) - .withSecurity(auth) - - lazy val deleteCommentEndpoint: SecuredEndpoint[DeleteCommentRequest, ScenarioActivityError, Unit, Any] = - baseNuApiEndpoint - .summary("Delete process comment service") - .tag("Scenario") - .delete - .in( - ("processes" / path[ProcessName]("scenarioName") / "activity" / "comments" - / path[Long]("commentId")).mapTo[DeleteCommentRequest] - ) - .out(statusCode(Ok)) - .errorOut( - oneOf[ScenarioActivityError]( - oneOfVariantFromMatchType( - NotFound, - plainBody[NoScenario] - .example( - Example.of( - summary = Some("No scenario {scenarioName} found"), - value = NoScenario(ProcessName("'example scenario'")) - ) - ) - ), - oneOfVariantFromMatchType( - InternalServerError, - plainBody[NoComment] - .example( - Example.of( - summary = Some("Unable to delete comment with id: {commentId}"), - value = NoComment(1L) - ) - ) - ) - ) - ) - .withSecurity(auth) - - def addAttachmentEndpoint( - implicit streamBodyEndpoint: EndpointInput[InputStream] - ): SecuredEndpoint[AddAttachmentRequest, ScenarioActivityError, Unit, Any] = { - baseNuApiEndpoint - .summary("Add scenario attachment service") - .tag("Scenario") - .post - .in( - ( - "processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" - / "attachments" / streamBodyEndpoint / header[FileName](HeaderNames.ContentDisposition) - ).mapTo[AddAttachmentRequest] - ) - .out(statusCode(Ok)) - .errorOut(scenarioNotFoundErrorOutput) - .withSecurity(auth) - } - - def downloadAttachmentEndpoint( - implicit streamBodyEndpoint: EndpointOutput[InputStream] - ): SecuredEndpoint[GetAttachmentRequest, ScenarioActivityError, GetAttachmentResponse, Any] = { - baseNuApiEndpoint - .summary("Download attachment service") - .tag("Scenario") - .get - .in( - ("processes" / path[ProcessName]("processName") / "activity" / "attachments" - / path[Long]("attachmentId")).mapTo[GetAttachmentRequest] - ) - .out( - statusCode(Ok) - .and(streamBodyEndpoint) - .and(header(HeaderNames.ContentDisposition)(optionalHeaderCodec)) - .and(header(HeaderNames.ContentType)(requiredHeaderCodec)) - .mapTo[GetAttachmentResponse] - ) - .errorOut(scenarioNotFoundErrorOutput) - .withSecurity(auth) - } - - private lazy val scenarioNotFoundErrorOutput: EndpointOutput.OneOf[ScenarioActivityError, ScenarioActivityError] = - oneOf[ScenarioActivityError]( - oneOfVariantFromMatchType( - NotFound, - plainBody[NoScenario] - .example( - Example.of( - summary = Some("No scenario {scenarioName} found"), - value = NoScenario(ProcessName("'example scenario'")) - ) - ) - ) - ) - -} - -object ScenarioActivityApiEndpoints { - - object Dtos { - @derive(encoder, decoder, schema) - final case class ScenarioActivity private (comments: List[Comment], attachments: List[Attachment]) - - object ScenarioActivity { - - def apply(activity: DbProcessActivity): ScenarioActivity = - new ScenarioActivity( - comments = activity.comments.map(Comment.apply), - attachments = activity.attachments.map(Attachment.apply) - ) - - } - - @derive(encoder, decoder, schema) - final case class Comment private ( - id: Long, - processVersionId: Long, - content: String, - user: String, - createDate: Instant - ) - - object Comment { - - def apply(comment: DbComment): Comment = - new Comment( - id = comment.id, - processVersionId = comment.processVersionId.value, - content = comment.content, - user = comment.user, - createDate = comment.createDate - ) - - } - - @derive(encoder, decoder, schema) - final case class Attachment private ( - id: Long, - processVersionId: Long, - fileName: String, - user: String, - createDate: Instant - ) - - object Attachment { - - def apply(attachment: DbAttachment): Attachment = - new Attachment( - id = attachment.id, - processVersionId = attachment.processVersionId.value, - fileName = attachment.fileName, - user = attachment.user, - createDate = attachment.createDate - ) - - } - - final case class AddCommentRequest(scenarioName: ProcessName, versionId: VersionId, commentContent: String) - - final case class DeleteCommentRequest(scenarioName: ProcessName, commentId: Long) - - final case class AddAttachmentRequest( - scenarioName: ProcessName, - versionId: VersionId, - body: InputStream, - fileName: FileName - ) - - final case class GetAttachmentRequest(scenarioName: ProcessName, attachmentId: Long) - - final case class GetAttachmentResponse(inputStream: InputStream, fileName: Option[String], contentType: String) - - object GetAttachmentResponse { - val emptyResponse: GetAttachmentResponse = - GetAttachmentResponse(InputStream.nullInputStream(), None, MediaType.TextPlainUtf8.toString()) - } - - sealed trait ScenarioActivityError - - object ScenarioActivityError { - final case class NoScenario(scenarioName: ProcessName) extends ScenarioActivityError - final case object NoPermission extends ScenarioActivityError with CustomAuthorizationError - 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" - ) - - implicit val noCommentCodec: Codec[String, NoComment, CodecFormat.TextPlain] = - BaseEndpointDefinitions.toTextPlainCodecSerializationOnly[NoComment](e => - s"Unable to delete comment with id: ${e.commentId}" - ) - - } - - } - -} 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 new file mode 100644 index 00000000000..bbae3da1bb6 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Dtos.scala @@ -0,0 +1,528 @@ +package pl.touk.nussknacker.ui.api.description.scenarioActivity + +import derevo.circe.{decoder, encoder} +import derevo.derive +import enumeratum.EnumEntry.UpperSnakecase +import enumeratum.{Enum, EnumEntry} +import io.circe +import io.circe.generic.extras +import io.circe.generic.extras.semiauto.deriveConfiguredCodec +import io.circe.{Decoder, Encoder} +import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} +import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions +import pl.touk.nussknacker.ui.api.BaseHttpService.CustomAuthorizationError +import pl.touk.nussknacker.ui.api.TapirCodecs.enumSchema +import pl.touk.nussknacker.ui.server.HeadersSupport.FileName +import sttp.model.MediaType +import sttp.tapir._ +import sttp.tapir.derevo.schema +import sttp.tapir.generic.Configuration + +import java.io.InputStream +import java.time.Instant +import java.util.UUID +import scala.collection.immutable + +object Dtos { + + @derive(encoder, decoder, schema) + final case class ScenarioActivitiesMetadata( + activities: List[ScenarioActivityMetadata], + actions: List[ScenarioActivityActionMetadata], + ) + + object ScenarioActivitiesMetadata { + + val default: ScenarioActivitiesMetadata = ScenarioActivitiesMetadata( + activities = ScenarioActivityType.values.map(ScenarioActivityMetadata.from).toList, + actions = List( + ScenarioActivityActionMetadata( + id = "compare", + displayableName = "Compare", + icon = "/assets/states/error.svg" + ), + ScenarioActivityActionMetadata( + id = "delete_comment", + displayableName = "Delete", + icon = "/assets/states/error.svg" + ), + ScenarioActivityActionMetadata( + id = "edit_comment", + displayableName = "Edit", + icon = "/assets/states/error.svg" + ), + ScenarioActivityActionMetadata( + id = "download_attachment", + displayableName = "Download", + icon = "/assets/states/error.svg" + ), + ScenarioActivityActionMetadata( + id = "delete_attachment", + displayableName = "Delete", + icon = "/assets/states/error.svg" + ), + ) + ) + + } + + @derive(encoder, decoder, schema) + final case class ScenarioActivityActionMetadata( + id: String, + displayableName: String, + icon: String, + ) + + @derive(encoder, decoder, schema) + final case class ScenarioActivityMetadata( + `type`: String, + displayableName: String, + icon: String, + supportedActions: List[String], + ) + + object ScenarioActivityMetadata { + + def from(scenarioActivityType: ScenarioActivityType): ScenarioActivityMetadata = + ScenarioActivityMetadata( + `type` = scenarioActivityType.entryName, + displayableName = scenarioActivityType.displayableName, + icon = scenarioActivityType.icon, + supportedActions = scenarioActivityType.supportedActions, + ) + + } + + sealed trait ScenarioActivityType extends EnumEntry with UpperSnakecase { + def displayableName: String + def icon: String + def supportedActions: List[String] + } + + object ScenarioActivityType extends Enum[ScenarioActivityType] { + + case object ScenarioCreated extends ScenarioActivityType { + override def displayableName: String = "Scenario created" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioArchived extends ScenarioActivityType { + override def displayableName: String = "Scenario archived" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioUnarchived extends ScenarioActivityType { + override def displayableName: String = "Scenario unarchived" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioDeployed extends ScenarioActivityType { + override def displayableName: String = "Deployment" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioPaused extends ScenarioActivityType { + override def displayableName: String = "Pause" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioCanceled extends ScenarioActivityType { + override def displayableName: String = "Cancel" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioModified extends ScenarioActivityType { + override def displayableName: String = "New version saved" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List("compare") + } + + case object ScenarioNameChanged extends ScenarioActivityType { + override def displayableName: String = "Scenario name changed" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object CommentAdded extends ScenarioActivityType { + override def displayableName: String = "Comment" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List("delete_comment", "edit_comment") + } + + case object AttachmentAdded extends ScenarioActivityType { + override def displayableName: String = "Attachment" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ChangedProcessingMode extends ScenarioActivityType { + override def displayableName: String = "Processing mode change" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object IncomingMigration extends ScenarioActivityType { + override def displayableName: String = "Incoming migration" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List("compare") + } + + case object OutgoingMigration extends ScenarioActivityType { + override def displayableName: String = "Outgoing migration" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object PerformedSingleExecution extends ScenarioActivityType { + override def displayableName: String = "Processing data" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object PerformedScheduledExecution extends ScenarioActivityType { + override def displayableName: String = "Processing data" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object AutomaticUpdate extends ScenarioActivityType { + override def displayableName: String = "Automatic update" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List("compare") + } + + case object CustomAction extends ScenarioActivityType { + override def displayableName: String = "Custom action" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + override def values: immutable.IndexedSeq[ScenarioActivityType] = findValues + + implicit def scenarioActivityTypeSchema: Schema[ScenarioActivityType] = + enumSchema[ScenarioActivityType]( + ScenarioActivityType.values.toList, + _.entryName, + ) + + implicit def scenarioActivityTypeCodec: circe.Codec[ScenarioActivityType] = circe.Codec.from( + Decoder.decodeString.emap(str => + ScenarioActivityType.withNameEither(str).left.map(_ => s"Invalid scenario action type [$str]") + ), + Encoder.encodeString.contramap(_.entryName), + ) + + implicit def scenarioActivityTypeTextCodec: Codec[String, ScenarioActivityType, CodecFormat.TextPlain] = + Codec.string.map( + Mapping.fromDecode[String, ScenarioActivityType] { + ScenarioActivityType.withNameOption(_) match { + case Some(value) => DecodeResult.Value(value) + case None => DecodeResult.InvalidValue(Nil) + } + }(_.entryName) + ) + + } + + @derive(encoder, decoder, schema) + final case class ScenarioActivityComment(comment: Option[String], lastModifiedBy: String, lastModifiedAt: Instant) + + @derive(encoder, decoder, schema) + final case class ScenarioActivityAttachment( + id: Option[Long], + filename: String, + lastModifiedBy: String, + lastModifiedAt: Instant + ) + + @derive(encoder, decoder, schema) + final case class ScenarioActivities(activities: List[ScenarioActivity]) + + sealed trait ScenarioActivity { + def id: UUID + def user: String + def date: Instant + def scenarioVersion: Option[Long] + } + + object ScenarioActivity { + + implicit def scenarioActivityCodec: circe.Codec[ScenarioActivity] = { + implicit val configuration: extras.Configuration = + extras.Configuration.default.withDiscriminator("type").withScreamingSnakeCaseConstructorNames + deriveConfiguredCodec + } + + implicit def scenarioActivitySchema: Schema[ScenarioActivity] = { + implicit val configuration: Configuration = + Configuration.default.withDiscriminator("type").withScreamingSnakeCaseDiscriminatorValues + Schema.derived[ScenarioActivity] + } + + final case class ScenarioCreated( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + ) extends ScenarioActivity + + final case class ScenarioArchived( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + ) extends ScenarioActivity + + final case class ScenarioUnarchived( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + ) extends ScenarioActivity + + // Scenario deployments + + final case class ScenarioDeployed( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + final case class ScenarioPaused( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + final case class ScenarioCanceled( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + // Scenario modifications + + final case class ScenarioModified( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + final case class ScenarioNameChanged( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + oldName: String, + newName: String, + ) extends ScenarioActivity + + final case class CommentAdded( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + final case class AttachmentAdded( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + attachment: ScenarioActivityAttachment, + ) extends ScenarioActivity + + final case class ChangedProcessingMode( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + from: String, + to: String, + ) extends ScenarioActivity + + // Migration between environments + + final case class IncomingMigration( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + sourceEnvironment: String, + sourceScenarioVersion: String, + ) extends ScenarioActivity + + final case class OutgoingMigration( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + destinationEnvironment: String, + ) extends ScenarioActivity + + // Batch + + final case class PerformedSingleExecution( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + dateFinished: Instant, + errorMessage: Option[String], + ) extends ScenarioActivity + + final case class PerformedScheduledExecution( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + dateFinished: Instant, + errorMessage: Option[String], + ) extends ScenarioActivity + + // Other/technical + + final case class AutomaticUpdate( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + dateFinished: Instant, + changes: String, + errorMessage: Option[String], + ) extends ScenarioActivity + + final case class CustomAction( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + actionName: String, + ) extends ScenarioActivity + + } + + @derive(encoder, decoder, schema) + final case class ScenarioAttachments(attachments: List[Attachment]) + + @derive(encoder, decoder, schema) + final case class Comment private ( + id: Long, + scenarioVersion: Long, + content: String, + user: String, + createDate: Instant + ) + + @derive(encoder, decoder, schema) + final case class Attachment private ( + id: Long, + scenarioVersion: Long, + fileName: String, + user: String, + createDate: Instant + ) + + final case class AddCommentRequest(scenarioName: ProcessName, versionId: VersionId, commentContent: String) + + final case class DeprecatedEditCommentRequest( + scenarioName: ProcessName, + commentId: Long, + commentContent: String + ) + + final case class EditCommentRequest( + scenarioName: ProcessName, + scenarioActivityId: UUID, + commentContent: String + ) + + final case class DeleteCommentRequest( + scenarioName: ProcessName, + scenarioActivityId: UUID + ) + + final case class DeprecatedDeleteCommentRequest( + scenarioName: ProcessName, + commentId: Long, + ) + + final case class AddAttachmentRequest( + scenarioName: ProcessName, + versionId: VersionId, + body: InputStream, + fileName: FileName + ) + + final case class GetAttachmentRequest(scenarioName: ProcessName, attachmentId: Long) + + final case class GetAttachmentResponse(inputStream: InputStream, fileName: Option[String], contentType: String) + + object GetAttachmentResponse { + val emptyResponse: GetAttachmentResponse = + GetAttachmentResponse(InputStream.nullInputStream(), None, MediaType.TextPlainUtf8.toString()) + } + + sealed trait ScenarioActivityError + + 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 + + implicit val noScenarioCodec: Codec[String, NoScenario, CodecFormat.TextPlain] = + BaseEndpointDefinitions.toTextPlainCodecSerializationOnly[NoScenario](e => s"No scenario ${e.scenarioName} found") + + implicit val noCommentCodec: Codec[String, NoComment, CodecFormat.TextPlain] = + BaseEndpointDefinitions.toTextPlainCodecSerializationOnly[NoComment](e => + s"Unable to delete comment with id: ${e.commentId}" + ) + + } + + object Legacy { + + @derive(encoder, decoder, schema) + final case class ProcessActivity private (comments: List[Comment], attachments: List[Attachment]) + + @derive(encoder, decoder, schema) + final case class Comment( + id: Long, + processVersionId: Long, + content: String, + user: String, + createDate: Instant + ) + + @derive(encoder, decoder, schema) + final case class Attachment( + id: Long, + processVersionId: Long, + fileName: String, + user: String, + createDate: Instant + ) + + } + +} 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 new file mode 100644 index 00000000000..a706ebd4dc4 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Endpoints.scala @@ -0,0 +1,192 @@ +package pl.touk.nussknacker.ui.api.description.scenarioActivity + +import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} +import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions +import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions.SecuredEndpoint +import pl.touk.nussknacker.security.AuthCredentials +import pl.touk.nussknacker.ui.api.TapirCodecs +import pl.touk.nussknacker.ui.server.HeadersSupport.FileName +import pl.touk.nussknacker.ui.server.TapirStreamEndpointProvider +import sttp.model.HeaderNames +import sttp.model.StatusCode.{InternalServerError, NotFound, Ok} +import sttp.tapir._ +import sttp.tapir.json.circe.jsonBody + +import java.util.UUID + +class Endpoints(auth: EndpointInput[AuthCredentials], streamProvider: TapirStreamEndpointProvider) + extends BaseEndpointDefinitions { + + import TapirCodecs.ContentDispositionCodec._ + import TapirCodecs.HeaderCodec._ + import TapirCodecs.ScenarioNameCodec._ + import TapirCodecs.VersionIdCodec._ + import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError._ + import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos._ + import pl.touk.nussknacker.ui.api.description.scenarioActivity.InputOutput._ + + lazy val deprecatedScenarioActivityEndpoint + : SecuredEndpoint[ProcessName, ScenarioActivityError, Legacy.ProcessActivity, Any] = + baseNuApiEndpoint + .summary("Scenario activity service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity") + .out(statusCode(Ok).and(jsonBody[Legacy.ProcessActivity].example(Examples.deprecatedScenarioActivity))) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + .deprecated() + + lazy val deprecatedAddCommentEndpoint: SecuredEndpoint[AddCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Add scenario comment service") + .tag("Activities") + .post + .in("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" / "comments") + .in(stringBody) + .mapInTo[AddCommentRequest] + .out(statusCode(Ok)) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + .deprecated() + + lazy val deprecatedDeleteCommentEndpoint + : SecuredEndpoint[DeprecatedDeleteCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Delete process comment service") + .tag("Activities") + .delete + .in( + "processes" / path[ProcessName]("scenarioName") / "activity" / "comments" / path[Long]("commentId") + ) + .mapInTo[DeprecatedDeleteCommentRequest] + .out(statusCode(Ok)) + .errorOut( + oneOf[ScenarioActivityError]( + oneOfVariantFromMatchType(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), + oneOfVariantFromMatchType(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)) + ) + ) + .withSecurity(auth) + .deprecated() + + lazy val scenarioActivitiesEndpoint: SecuredEndpoint[ + ProcessName, + ScenarioActivityError, + ScenarioActivities, + Any + ] = + baseNuApiEndpoint + .summary("Scenario activities service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity" / "activities") + .out(statusCode(Ok).and(jsonBody[ScenarioActivities].example(Examples.scenarioActivities))) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + + lazy val scenarioActivitiesMetadataEndpoint + : SecuredEndpoint[ProcessName, ScenarioActivityError, ScenarioActivitiesMetadata, Any] = + baseNuApiEndpoint + .summary("Scenario activities metadata service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity" / "activities" / "metadata") + .out(statusCode(Ok).and(jsonBody[ScenarioActivitiesMetadata].example(ScenarioActivitiesMetadata.default))) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + + lazy val addCommentEndpoint: SecuredEndpoint[AddCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Add scenario comment service") + .tag("Activities") + .post + .in("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" / "comment") + .in(stringBody) + .mapInTo[AddCommentRequest] + .out(statusCode(Ok)) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + + lazy val editCommentEndpoint: SecuredEndpoint[EditCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Edit process comment service") + .tag("Activities") + .put + .in( + "processes" / path[ProcessName]("scenarioName") / "activity" / "comment" / path[UUID]("scenarioActivityId") + ) + .in(stringBody) + .mapInTo[EditCommentRequest] + .out(statusCode(Ok)) + .errorOut( + oneOf[ScenarioActivityError]( + oneOfVariantFromMatchType(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), + oneOfVariantFromMatchType(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)) + ) + ) + .withSecurity(auth) + + lazy val deleteCommentEndpoint: SecuredEndpoint[DeleteCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Delete process comment service") + .tag("Activities") + .delete + .in( + "processes" / path[ProcessName]("scenarioName") / "activity" / "comment" / path[UUID]("scenarioActivityId") + ) + .mapInTo[DeleteCommentRequest] + .out(statusCode(Ok)) + .errorOut( + oneOf[ScenarioActivityError]( + oneOfVariantFromMatchType(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), + oneOfVariantFromMatchType(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)) + ) + ) + .withSecurity(auth) + + val attachmentsEndpoint: SecuredEndpoint[ProcessName, ScenarioActivityError, ScenarioAttachments, Any] = { + baseNuApiEndpoint + .summary("Scenario attachments service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity" / "attachments") + .out(statusCode(Ok).and(jsonBody[ScenarioAttachments].example(Examples.scenarioAttachments))) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + } + + val addAttachmentEndpoint: SecuredEndpoint[AddAttachmentRequest, ScenarioActivityError, Unit, Any] = { + baseNuApiEndpoint + .summary("Add scenario attachment service") + .tag("Activities") + .post + .in("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" / "attachments") + .in(streamProvider.streamBodyEndpointInput) + .in(header[FileName](HeaderNames.ContentDisposition)) + .mapInTo[AddAttachmentRequest] + .out(statusCode(Ok)) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + } + + val downloadAttachmentEndpoint + : SecuredEndpoint[GetAttachmentRequest, ScenarioActivityError, GetAttachmentResponse, Any] = { + baseNuApiEndpoint + .summary("Download attachment service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity" / "attachments" / path[Long]("attachmentId")) + .mapInTo[GetAttachmentRequest] + .out( + statusCode(Ok) + .and(streamProvider.streamBodyEndpointOutput) + .and(header(HeaderNames.ContentDisposition)(optionalHeaderCodec)) + .and(header(HeaderNames.ContentType)(requiredHeaderCodec)) + .mapTo[GetAttachmentResponse] + ) + .errorOut(scenarioNotFoundErrorOutput) + .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 new file mode 100644 index 00000000000..1050cd95c99 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Examples.scala @@ -0,0 +1,236 @@ +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._ +import sttp.tapir.EndpointIO.Example + +import java.time.Instant +import java.util.UUID + +object Examples { + + val deprecatedScenarioActivity: Example[Legacy.ProcessActivity] = Example.of( + summary = Some("Display scenario activity"), + value = Legacy.ProcessActivity( + comments = List( + Legacy.Comment( + id = 1L, + processVersionId = 1L, + content = "some comment", + user = "test", + createDate = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + attachments = List( + Legacy.Attachment( + id = 1L, + processVersionId = 1L, + fileName = "some_file.txt", + user = "test", + createDate = Instant.parse("2024-01-17T14:21:17Z") + ) + ) + ) + ) + + val scenarioActivities: Example[ScenarioActivities] = Example.of( + summary = Some("Display scenario actions"), + value = ScenarioActivities( + activities = List( + ScenarioActivity.ScenarioCreated( + id = UUID.fromString("80c95497-3b53-4435-b2d9-ae73c5766213"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + ), + ScenarioActivity.ScenarioArchived( + id = UUID.fromString("070a4e5c-21e5-4e63-acac-0052cf705a90"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + ), + ScenarioActivity.ScenarioUnarchived( + id = UUID.fromString("fa35d944-fe20-4c4f-96c6-316b6197951a"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + ), + ScenarioActivity.ScenarioDeployed( + id = UUID.fromString("545b7d87-8cdf-4cb5-92c4-38ddbfca3d08"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Deployment of scenario - task JIRA-1234"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + ScenarioActivity.ScenarioCanceled( + id = UUID.fromString("c354eba1-de97-455c-b977-74729c41ce7"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Canceled because marketing campaign ended"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + ScenarioActivity.ScenarioModified( + id = UUID.fromString("07b04d45-c7c0-4980-a3bc-3c7f66410f68"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Added new processing step"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + ScenarioActivity.ScenarioNameChanged( + id = UUID.fromString("da3d1f78-7d73-4ed9-b0e5-95538e150d0d"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + oldName = "marketing campaign", + newName = "old marketing campaign", + ), + ScenarioActivity.CommentAdded( + id = UUID.fromString("edf8b047-9165-445d-a173-ba61812dbd63"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Added new processing step"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + ScenarioActivity.CommentAdded( + id = UUID.fromString("369367d6-d445-4327-ac23-4a94367b1d9e"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = None, + lastModifiedBy = "John Doe", + lastModifiedAt = Instant.parse("2024-01-18T14:21:17Z") + ) + ), + ScenarioActivity.AttachmentAdded( + id = UUID.fromString("b29916a9-34d4-4fc2-a6ab-79569f68c0b2"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + attachment = ScenarioActivityAttachment( + id = Some(10000001), + filename = "attachment01.png", + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ), + ), + ScenarioActivity.AttachmentAdded( + id = UUID.fromString("d0a7f4a2-abcc-4ffa-b1ca-68f6da3e999a"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + attachment = ScenarioActivityAttachment( + id = None, + filename = "attachment01.png", + lastModifiedBy = "John Doe", + lastModifiedAt = Instant.parse("2024-01-18T14:21:17Z") + ), + ), + ScenarioActivity.ChangedProcessingMode( + id = UUID.fromString("683df470-0b33-4ead-bf61-fa35c63484f3"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + from = "Request-Response", + to = "Batch", + ), + ScenarioActivity.IncomingMigration( + id = UUID.fromString("4da0f1ac-034a-49b6-81c9-8ee48ba1d830"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + sourceEnvironment = "preprod", + sourceScenarioVersion = "23", + ), + ScenarioActivity.OutgoingMigration( + id = UUID.fromString("49fcd45d-3fa6-48d4-b8ed-b3055910c7ad"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Added new processing step"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ), + destinationEnvironment = "preprod", + ), + ScenarioActivity.PerformedSingleExecution( + id = UUID.fromString("924dfcd3-fbc7-44ea-8763-813874382204"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + errorMessage = Some("Execution error occurred"), + ), + ScenarioActivity.PerformedSingleExecution( + id = UUID.fromString("924dfcd3-fbc7-44ea-8763-813874382204"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + errorMessage = None, + ), + ScenarioActivity.PerformedScheduledExecution( + id = UUID.fromString("9b27797e-aa03-42ba-8406-d0ae8005a883"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + errorMessage = None, + ), + ScenarioActivity.AutomaticUpdate( + id = UUID.fromString("33509d37-7657-4229-940f-b5736c82fb13"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + changes = "JIRA-12345, JIRA-32146", + errorMessage = None, + ), + ), + ) + ) + + val scenarioAttachments: Example[ScenarioAttachments] = Example.of( + summary = Some("Display scenario activity"), + value = ScenarioAttachments( + attachments = List( + Attachment( + id = 1L, + scenarioVersion = 1L, + fileName = "some_file.txt", + user = "test", + createDate = Instant.parse("2024-01-17T14:21:17Z") + ) + ) + ) + ) + + val noScenarioError: Example[NoScenario] = Example.of( + summary = Some("No scenario {scenarioName} found"), + value = NoScenario(ProcessName("'example scenario'")) + ) + + val commentNotFoundError: Example[NoComment] = Example.of( + summary = Some("Unable to edit comment with id: {commentId}"), + value = NoComment("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 new file mode 100644 index 00000000000..701f74bab3e --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/InputOutput.scala @@ -0,0 +1,30 @@ +package pl.touk.nussknacker.ui.api.description.scenarioActivity + +import pl.touk.nussknacker.engine.api.process.ProcessName +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError.NoScenario +import sttp.model.StatusCode.{NotFound, NotImplemented} +import sttp.tapir.EndpointIO.Example +import sttp.tapir.{EndpointOutput, emptyOutputAs, oneOf, oneOfVariantFromMatchType, plainBody} + +object InputOutput { + + val scenarioNotFoundErrorOutput: EndpointOutput.OneOf[ScenarioActivityError, ScenarioActivityError] = + oneOf[ScenarioActivityError]( + oneOfVariantFromMatchType( + NotFound, + plainBody[NoScenario] + .example( + Example.of( + summary = Some("No scenario {scenarioName} found"), + value = NoScenario(ProcessName("'example scenario'")) + ) + ) + ), + oneOfVariantFromMatchType( + NotImplemented, + emptyOutputAs(ScenarioActivityError.NotImplemented), + ) + ) + +} diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala index 49ade0644d3..7c0bc88ff69 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala @@ -1,5 +1,7 @@ package pl.touk.nussknacker.ui.api +import akka.actor.ActorSystem +import akka.stream.Materializer import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers import org.scalatest.prop.TableDrivenPropertyChecks._ @@ -8,6 +10,7 @@ import pl.touk.nussknacker.security.AuthCredentials.PassedAuthCredentials import pl.touk.nussknacker.test.utils.domain.ReflectionBasedUtils import pl.touk.nussknacker.test.utils.{InvalidExample, OpenAPIExamplesValidator, OpenAPISchemaComponents} import pl.touk.nussknacker.ui.security.api.AuthManager.ImpersonationConsideringInputEndpoint +import pl.touk.nussknacker.ui.server.{AkkaHttpBasedTapirStreamEndpointProvider, TapirStreamEndpointProvider} import pl.touk.nussknacker.ui.services.NuDesignerExposedApiHttpService import pl.touk.nussknacker.ui.util.Project import sttp.apispec.openapi.circe.yaml.RichOpenAPI @@ -15,6 +18,7 @@ import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter import sttp.tapir.{Endpoint, EndpointInput, auth} import java.lang.reflect.{Method, Modifier} +import scala.concurrent.Await import scala.util.Try // if the test fails it probably means that you should regenerate the Nu Designer OpenAPI document @@ -138,30 +142,46 @@ class NuDesignerApiAvailableToExposeYamlSpec extends AnyFunSuite with Matchers { object NuDesignerApiAvailableToExpose { - def generateOpenApiYaml: String = { - val endpoints = findApiEndpointsClasses().flatMap(findEndpointsInClass) + def generateOpenApiYaml: String = withStreamProvider { streamProvider => + val endpoints = findApiEndpointsClasses().flatMap(findEndpointsInClass(streamProvider)) val docs = OpenAPIDocsInterpreter(NuDesignerExposedApiHttpService.openAPIDocsOptions).toOpenAPI( es = endpoints, title = NuDesignerExposedApiHttpService.openApiDocumentTitle, version = "" ) - docs.toYaml } + private def withStreamProvider[T](handle: TapirStreamEndpointProvider => T): T = { + val actorSystem: ActorSystem = ActorSystem() + val mat: Materializer = Materializer(actorSystem) + val streamProvider: TapirStreamEndpointProvider = new AkkaHttpBasedTapirStreamEndpointProvider()(mat) + val result = handle(streamProvider) + Await.result(actorSystem.terminate(), scala.concurrent.duration.Duration.Inf) + result + } + private def findApiEndpointsClasses() = { ReflectionBasedUtils.findSubclassesOf[BaseEndpointDefinitions]("pl.touk.nussknacker.ui.api") } - private def findEndpointsInClass(clazz: Class[_ <: BaseEndpointDefinitions]) = { - val endpointDefinitions = createInstanceOf(clazz) + private def findEndpointsInClass( + streamEndpointProvider: TapirStreamEndpointProvider + )( + clazz: Class[_ <: BaseEndpointDefinitions] + ) = { + val endpointDefinitions = createInstanceOf(streamEndpointProvider)(clazz) clazz.getDeclaredMethods.toList .filter(isEndpointMethod) .sortBy(_.getName) .map(instantiateEndpointDefinition(endpointDefinitions, _)) } - private def createInstanceOf(clazz: Class[_ <: BaseEndpointDefinitions]) = { + private def createInstanceOf( + streamEndpointProvider: TapirStreamEndpointProvider, + )( + clazz: Class[_ <: BaseEndpointDefinitions], + ) = { val basicAuth = auth .basic[Option[String]]() .map(_.map(PassedAuthCredentials))(_.map(_.value)) @@ -173,6 +193,10 @@ object NuDesignerApiAvailableToExpose { Try(clazz.getDeclaredConstructor()) .map(_.newInstance()) } + .orElse { + Try(clazz.getConstructor(classOf[EndpointInput[PassedAuthCredentials]], classOf[TapirStreamEndpointProvider])) + .map(_.newInstance(basicAuth, streamEndpointProvider)) + } .getOrElse( throw new IllegalStateException( s"Class ${clazz.getName} is required to have either one parameter constructor or constructor without parameters" diff --git a/docs-internal/api/nu-designer-openapi.yaml b/docs-internal/api/nu-designer-openapi.yaml index 5e78df23cba..eeda77e9167 100644 --- a/docs-internal/api/nu-designer-openapi.yaml +++ b/docs-internal/api/nu-designer-openapi.yaml @@ -2249,12 +2249,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/{versionId}/activity/comments: - post: + /api/scenarioParametersCombinations: + get: tags: - - Scenario - summary: Add scenario comment service - operationId: postApiProcessesScenarionameVersionidActivityComments + - App + summary: Service providing available combinations of scenario's parameters + operationId: getApiScenarioparameterscombinations parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2263,29 +2263,32 @@ paths: type: - string - 'null' - - name: scenarioName - in: path - required: true - schema: - type: string - - name: versionId - in: path - required: true - schema: - type: integer - format: int64 - requestBody: - content: - text/plain: - schema: - type: string - required: true responses: '200': description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScenarioParametersCombinationWithEngineErrors' + examples: + Example: + summary: List of available parameters combinations + value: + combinations: + - processingMode: Unbounded-Stream + category: Marketing + engineSetupName: Flink + - processingMode: Request-Response + category: Fraud + engineSetupName: Lite K8s + - processingMode: Unbounded-Stream + category: Fraud + engineSetupName: Flink Fraud Detection + engineSetupErrors: + Flink: + - Invalid Flink configuration '400': - description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter versionId, Invalid value for: body' + description: 'Invalid value for: header Nu-Impersonate-User-Identity' content: text/plain: schema: @@ -2320,8 +2323,8 @@ paths: type: string examples: Example: - summary: No scenario {scenarioName} found - value: No scenario 'example scenario' found + summary: No impersonated user's data found for provided identity + value: No impersonated user data found for provided identity '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2336,12 +2339,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/comments/{commentId}: - delete: + /api/statistic: + post: tags: - - Scenario - summary: Delete process comment service - operationId: deleteApiProcessesScenarionameActivityCommentsCommentid + - Statistics + summary: Register statistics service + operationId: postApiStatistic parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2350,23 +2353,18 @@ paths: type: - string - 'null' - - name: scenarioName - in: path - required: true - schema: - type: string - - name: commentId - in: path + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterStatisticsRequestDto' required: true - schema: - type: integer - format: int64 responses: - '200': + '204': description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter commentId' + value for: body' content: text/plain: schema: @@ -2401,18 +2399,8 @@ paths: type: string examples: Example: - summary: No scenario {scenarioName} found - value: No scenario 'example scenario' found - '500': - description: '' - content: - text/plain: - schema: - type: string - examples: - Example: - summary: 'Unable to delete comment with id: {commentId}' - value: 'Unable to delete comment with id: 1' + summary: No impersonated user's data found for provided identity + value: No impersonated user data found for provided identity '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2427,12 +2415,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity: + /api/statistic/usage: get: tags: - - Scenario - summary: Scenario activity service - operationId: getApiProcessesScenarionameActivity + - Statistics + summary: Statistics URL service + operationId: getApiStatisticUsage parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2441,34 +2429,19 @@ paths: type: - string - 'null' - - name: scenarioName - in: path - required: true - schema: - type: string responses: '200': description: '' content: application/json: schema: - $ref: '#/components/schemas/ScenarioActivity' + $ref: '#/components/schemas/StatisticUrlResponseDto' examples: Example: - summary: Display scenario activity + summary: List of statistics URLs value: - comments: - - id: 1 - processVersionId: 1 - content: some comment - user: test - createDate: '2024-01-17T14:21:17Z' - attachments: - - id: 1 - processVersionId: 1 - fileName: some_file.txt - user: test - createDate: '2024-01-17T14:21:17Z' + urls: + - https://stats.nussknacker.io/?a_n=1&a_t=0&a_v=0&c=3&c_n=82&c_t=0&c_v=0&f_m=0&f_v=0&fingerprint=development&n_m=2&n_ma=0&n_mi=2&n_v=1&s_a=0&s_dm_c=1&s_dm_e=1&s_dm_f=2&s_dm_l=0&s_f=1&s_pm_b=0&s_pm_rr=1&s_pm_s=3&s_s=3&source=sources&u_ma=0&u_mi=0&u_v=0&v_m=2&v_ma=1&v_mi=3&v_v=2&version=1.15.0-SNAPSHOT '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity' content: @@ -2505,8 +2478,18 @@ paths: type: string examples: Example: - summary: No scenario {scenarioName} found - value: No scenario 'example scenario' found + summary: No impersonated user's data found for provided identity + value: No impersonated user data found for provided identity + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Statistics generation failed. + value: Statistics generation failed. '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2521,12 +2504,12 @@ paths: security: - {} - httpAuth: [] - /api/scenarioParametersCombinations: + /api/user: get: tags: - - App - summary: Service providing available combinations of scenario's parameters - operationId: getApiScenarioparameterscombinations + - User + summary: Logged user info service + operationId: getApiUser parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2541,24 +2524,31 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ScenarioParametersCombinationWithEngineErrors' + $ref: '#/components/schemas/DisplayableUser' examples: - Example: - summary: List of available parameters combinations + Example0: + summary: Common user info value: - combinations: - - processingMode: Unbounded-Stream - category: Marketing - engineSetupName: Flink - - processingMode: Request-Response - category: Fraud - engineSetupName: Lite K8s - - processingMode: Unbounded-Stream - category: Fraud - engineSetupName: Flink Fraud Detection - engineSetupErrors: - Flink: - - Invalid Flink configuration + id: reader + username: reader + isAdmin: false + categories: + - Category1 + categoryPermissions: + Category1: + - Read + globalPermissions: [] + Example1: + summary: Admin user info + value: + id: admin + username: admin + isAdmin: true + categories: + - Category1 + - Category2 + categoryPermissions: {} + globalPermissions: [] '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity' content: @@ -2611,12 +2601,12 @@ paths: security: - {} - httpAuth: [] - /api/statistic: + /api/processes/{scenarioName}/{versionId}/activity/attachments: post: tags: - - Statistics - summary: Register statistics service - operationId: postApiStatistic + - Activities + summary: Add scenario attachment service + operationId: postApiProcessesScenarionameVersionidActivityAttachments parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2625,18 +2615,36 @@ paths: type: - string - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: versionId + in: path + required: true + schema: + type: integer + format: int64 + - name: Content-Disposition + in: header + required: true + schema: + type: string requestBody: content: - application/json: + application/octet-stream: schema: - $ref: '#/components/schemas/RegisterStatisticsRequestDto' + type: string + format: binary required: true responses: - '204': + '200': description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: body' + value for: path parameter versionId, Invalid value for: body, Invalid + value for: header Content-Disposition' content: text/plain: schema: @@ -2671,8 +2679,8 @@ paths: type: string examples: Example: - summary: No impersonated user's data found for provided identity - value: No impersonated user data found for provided identity + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2687,12 +2695,12 @@ paths: security: - {} - httpAuth: [] - /api/statistic/usage: - get: + /api/processes/{scenarioName}/{versionId}/activity/comment: + post: tags: - - Statistics - summary: Statistics URL service - operationId: getApiStatisticUsage + - Activities + summary: Add scenario comment service + operationId: postApiProcessesScenarionameVersionidActivityComment parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2701,21 +2709,29 @@ paths: type: - string - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: versionId + in: path + required: true + schema: + type: integer + format: int64 + requestBody: + content: + text/plain: + schema: + type: string + required: true responses: '200': description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/StatisticUrlResponseDto' - examples: - Example: - summary: List of statistics URLs - value: - urls: - - https://stats.nussknacker.io/?a_n=1&a_t=0&a_v=0&c=3&c_n=82&c_t=0&c_v=0&f_m=0&f_v=0&fingerprint=development&n_m=2&n_ma=0&n_mi=2&n_v=1&s_a=0&s_dm_c=1&s_dm_e=1&s_dm_f=2&s_dm_l=0&s_f=1&s_pm_b=0&s_pm_rr=1&s_pm_s=3&s_s=3&source=sources&u_ma=0&u_mi=0&u_v=0&v_m=2&v_ma=1&v_mi=3&v_v=2&version=1.15.0-SNAPSHOT '400': - description: 'Invalid value for: header Nu-Impersonate-User-Identity' + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter versionId, Invalid value for: body' content: text/plain: schema: @@ -2750,18 +2766,873 @@ paths: type: string examples: Example: - summary: No impersonated user's data found for provided identity - value: No impersonated user data found for provided identity - '500': - description: '' - content: - text/plain: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/attachments: + get: + tags: + - Activities + summary: Scenario attachments service + operationId: getApiProcessesScenarionameActivityAttachments + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScenarioAttachments' + examples: + Example: + summary: Display scenario activity + value: + attachments: + - id: 1 + scenarioVersion: 1 + fileName: some_file.txt + user: test + createDate: '2024-01-17T14:21:17Z' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/comment/{scenarioActivityId}: + put: + tags: + - Activities + summary: Edit process comment service + operationId: putApiProcessesScenarionameActivityCommentScenarioactivityid + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: scenarioActivityId + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + text/plain: + schema: + type: string + required: true + responses: + '200': + description: '' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter scenarioActivityId, Invalid value for: body' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + delete: + tags: + - Activities + summary: Delete process comment service + operationId: deleteApiProcessesScenarionameActivityCommentScenarioactivityid + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: scenarioActivityId + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: '' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter scenarioActivityId' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/{versionId}/activity/comments: + post: + tags: + - Activities + summary: Add scenario comment service + operationId: postApiProcessesScenarionameVersionidActivityComments + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: versionId + in: path + required: true + schema: + type: integer + format: int64 + requestBody: + content: + text/plain: + schema: + type: string + required: true + responses: + '200': + description: '' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter versionId, Invalid value for: body' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + deprecated: true + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/comments/{commentId}: + delete: + tags: + - Activities + summary: Delete process comment service + operationId: deleteApiProcessesScenarionameActivityCommentsCommentid + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: commentId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: '' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter commentId' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + deprecated: true + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity: + get: + tags: + - Activities + summary: Scenario activity service + operationId: getApiProcessesScenarionameActivity + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessActivity' + examples: + Example: + summary: Display scenario activity + value: + comments: + - id: 1 + processVersionId: 1 + content: some comment + user: test + createDate: '2024-01-17T14:21:17Z' + attachments: + - id: 1 + processVersionId: 1 + fileName: some_file.txt + user: test + createDate: '2024-01-17T14:21:17Z' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + deprecated: true + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/attachments/{attachmentId}: + get: + tags: + - Activities + summary: Download attachment service + operationId: getApiProcessesScenarionameActivityAttachmentsAttachmentid + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: attachmentId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: '' + headers: + Content-Disposition: + required: false + schema: + type: + - string + - 'null' + Content-Type: + required: true + schema: + type: string + content: + application/octet-stream: + schema: + type: string + format: binary + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter attachmentId' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/activities: + get: + tags: + - Activities + summary: Scenario activities service + operationId: getApiProcessesScenarionameActivityActivities + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScenarioActivities' + examples: + Example: + summary: Display scenario actions + value: + activities: + - id: 80c95497-3b53-4435-b2d9-ae73c5766213 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + type: SCENARIO_CREATED + - id: 070a4e5c-21e5-4e63-acac-0052cf705a90 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + type: SCENARIO_ARCHIVED + - id: fa35d944-fe20-4c4f-96c6-316b6197951a + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + type: SCENARIO_UNARCHIVED + - id: 545b7d87-8cdf-4cb5-92c4-38ddbfca3d08 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Deployment of scenario - task JIRA-1234 + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: SCENARIO_DEPLOYED + - id: c354eba1-de97-455c-b977-074729c41ce7 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Canceled because marketing campaign ended + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: SCENARIO_CANCELED + - id: 07b04d45-c7c0-4980-a3bc-3c7f66410f68 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Added new processing step + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: SCENARIO_MODIFIED + - id: da3d1f78-7d73-4ed9-b0e5-95538e150d0d + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + oldName: marketing campaign + newName: old marketing campaign + type: SCENARIO_NAME_CHANGED + - id: edf8b047-9165-445d-a173-ba61812dbd63 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Added new processing step + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: COMMENT_ADDED + - id: 369367d6-d445-4327-ac23-4a94367b1d9e + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + lastModifiedBy: John Doe + lastModifiedAt: '2024-01-18T14:21:17Z' + type: COMMENT_ADDED + - id: b29916a9-34d4-4fc2-a6ab-79569f68c0b2 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + attachment: + id: 10000001 + filename: attachment01.png + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: ATTACHMENT_ADDED + - id: d0a7f4a2-abcc-4ffa-b1ca-68f6da3e999a + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + attachment: + filename: attachment01.png + lastModifiedBy: John Doe + lastModifiedAt: '2024-01-18T14:21:17Z' + type: ATTACHMENT_ADDED + - id: 683df470-0b33-4ead-bf61-fa35c63484f3 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + from: Request-Response + to: Batch + type: CHANGED_PROCESSING_MODE + - id: 4da0f1ac-034a-49b6-81c9-8ee48ba1d830 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + sourceEnvironment: preprod + sourceScenarioVersion: '23' + type: INCOMING_MIGRATION + - id: 49fcd45d-3fa6-48d4-b8ed-b3055910c7ad + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Added new processing step + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + destinationEnvironment: preprod + type: OUTGOING_MIGRATION + - id: 924dfcd3-fbc7-44ea-8763-813874382204 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + dateFinished: '2024-01-17T14:21:17Z' + errorMessage: Execution error occurred + type: PERFORMED_SINGLE_EXECUTION + - id: 924dfcd3-fbc7-44ea-8763-813874382204 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + dateFinished: '2024-01-17T14:21:17Z' + type: PERFORMED_SINGLE_EXECUTION + - id: 9b27797e-aa03-42ba-8406-d0ae8005a883 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + dateFinished: '2024-01-17T14:21:17Z' + type: PERFORMED_SCHEDULED_EXECUTION + - id: 33509d37-7657-4229-940f-b5736c82fb13 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + dateFinished: '2024-01-17T14:21:17Z' + changes: JIRA-12345, JIRA-32146 + type: AUTOMATIC_UPDATE + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: schema: type: string examples: Example: - summary: Statistics generation failed. - value: Statistics generation failed. + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2776,12 +3647,12 @@ paths: security: - {} - httpAuth: [] - /api/user: + /api/processes/{scenarioName}/activity/activities/metadata: get: tags: - - User - summary: Logged user info service - operationId: getApiUser + - Activities + summary: Scenario activities metadata service + operationId: getApiProcessesScenarionameActivityActivitiesMetadata parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2790,37 +3661,109 @@ paths: type: - string - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string responses: '200': description: '' content: application/json: schema: - $ref: '#/components/schemas/DisplayableUser' - examples: - Example0: - summary: Common user info - value: - id: reader - username: reader - isAdmin: false - categories: - - Category1 - categoryPermissions: - Category1: - - Read - globalPermissions: [] - Example1: - summary: Admin user info - value: - id: admin - username: admin - isAdmin: true - categories: - - Category1 - - Category2 - categoryPermissions: {} - globalPermissions: [] + $ref: '#/components/schemas/ScenarioActivitiesMetadata' + example: + activities: + - type: SCENARIO_CREATED + displayableName: Scenario created + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_ARCHIVED + displayableName: Scenario archived + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_UNARCHIVED + displayableName: Scenario unarchived + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_DEPLOYED + displayableName: Deployment + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_PAUSED + displayableName: Pause + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_CANCELED + displayableName: Cancel + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_MODIFIED + displayableName: New version saved + icon: /assets/states/error.svg + supportedActions: + - compare + - type: SCENARIO_NAME_CHANGED + displayableName: Scenario name changed + icon: /assets/states/error.svg + supportedActions: [] + - type: COMMENT_ADDED + displayableName: Comment + icon: /assets/states/error.svg + supportedActions: + - delete_comment + - edit_comment + - type: ATTACHMENT_ADDED + displayableName: Attachment + icon: /assets/states/error.svg + supportedActions: [] + - type: CHANGED_PROCESSING_MODE + displayableName: Processing mode change + icon: /assets/states/error.svg + supportedActions: [] + - type: INCOMING_MIGRATION + displayableName: Incoming migration + icon: /assets/states/error.svg + supportedActions: + - compare + - type: OUTGOING_MIGRATION + displayableName: Outgoing migration + icon: /assets/states/error.svg + supportedActions: [] + - type: PERFORMED_SINGLE_EXECUTION + displayableName: Processing data + icon: /assets/states/error.svg + supportedActions: [] + - type: PERFORMED_SCHEDULED_EXECUTION + displayableName: Processing data + icon: /assets/states/error.svg + supportedActions: [] + - type: AUTOMATIC_UPDATE + displayableName: Automatic update + icon: /assets/states/error.svg + supportedActions: + - compare + - type: CUSTOM_ACTION + displayableName: Custom action + icon: /assets/states/error.svg + supportedActions: [] + actions: + - id: compare + displayableName: Compare + icon: /assets/states/error.svg + - id: delete_comment + displayableName: Delete + icon: /assets/states/error.svg + - id: edit_comment + displayableName: Edit + icon: /assets/states/error.svg + - id: download_attachment + displayableName: Download + icon: /assets/states/error.svg + - id: delete_attachment + displayableName: Delete + icon: /assets/states/error.svg '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity' content: @@ -2857,8 +3800,8 @@ paths: type: string examples: Example: - summary: No impersonated user's data found for provided identity - value: No impersonated user data found for provided identity + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2889,6 +3832,29 @@ components: type: integer format: int32 Attachment: + title: Attachment + type: object + required: + - id + - scenarioVersion + - fileName + - user + - createDate + properties: + id: + type: integer + format: int64 + scenarioVersion: + type: integer + format: int64 + fileName: + type: string + user: + type: string + createDate: + type: string + format: date-time + Attachment1: title: Attachment type: object required: @@ -2911,6 +3877,68 @@ components: createDate: type: string format: date-time + AttachmentAdded: + title: AttachmentAdded + type: object + required: + - id + - user + - date + - attachment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + attachment: + $ref: '#/components/schemas/ScenarioActivityAttachment' + type: + type: string + AutomaticUpdate: + title: AutomaticUpdate + type: object + required: + - id + - user + - date + - dateFinished + - changes + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + dateFinished: + type: string + format: date-time + changes: + type: string + errorMessage: + type: + - string + - 'null' + type: + type: string BoolParameterEditor: title: BoolParameterEditor type: object @@ -2989,6 +4017,36 @@ components: format: int32 errorMessage: type: string + ChangedProcessingMode: + title: ChangedProcessingMode + type: object + required: + - id + - user + - date + - from + - to + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + from: + type: string + to: + type: string + type: + type: string ColumnDefinition: title: ColumnDefinition type: object @@ -3023,6 +4081,33 @@ components: createDate: type: string format: date-time + CommentAdded: + title: CommentAdded + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string ComponentLink: title: ComponentLink type: object @@ -3130,6 +4215,33 @@ components: CronParameterEditor: title: CronParameterEditor type: object + CustomAction: + title: CustomAction + type: object + required: + - id + - user + - date + - actionName + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + actionName: + type: string + type: + type: string CustomActionRequest: title: CustomActionRequest type: object @@ -3545,6 +4657,36 @@ components: uniqueItems: true items: type: string + IncomingMigration: + title: IncomingMigration + type: object + required: + - id + - user + - date + - sourceEnvironment + - sourceScenarioVersion + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + sourceEnvironment: + type: string + sourceScenarioVersion: + type: string + type: + type: string JsonParameterEditor: title: JsonParameterEditor type: object @@ -4186,6 +5328,36 @@ components: - info - success - error + OutgoingMigration: + title: OutgoingMigration + type: object + required: + - id + - user + - date + - comment + - destinationEnvironment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + destinationEnvironment: + type: string + type: + type: string Parameter: title: Parameter type: object @@ -4270,14 +5442,78 @@ components: title: ParametersValidationResultDto type: object required: - - validationPerformed + - validationPerformed + properties: + validationErrors: + type: array + items: + $ref: '#/components/schemas/NodeValidationError' + validationPerformed: + type: boolean + PerformedScheduledExecution: + title: PerformedScheduledExecution + type: object + required: + - id + - user + - date + - dateFinished + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + dateFinished: + type: string + format: date-time + errorMessage: + type: + - string + - 'null' + type: + type: string + PerformedSingleExecution: + title: PerformedSingleExecution + type: object + required: + - id + - user + - date + - dateFinished + - type properties: - validationErrors: - type: array - items: - $ref: '#/components/schemas/NodeValidationError' - validationPerformed: - type: boolean + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + dateFinished: + type: string + format: date-time + errorMessage: + type: + - string + - 'null' + type: + type: string PeriodParameterEditor: title: PeriodParameterEditor type: object @@ -4352,6 +5588,18 @@ components: - FINISHED - FAILED - EXECUTION_FINISHED + ProcessActivity: + title: ProcessActivity + type: object + properties: + comments: + type: array + items: + $ref: '#/components/schemas/Comment' + attachments: + type: array + items: + $ref: '#/components/schemas/Attachment1' ProcessAdditionalFields: title: ProcessAdditionalFields type: object @@ -4415,18 +5663,301 @@ components: type: - string - 'null' + ScenarioActivities: + title: ScenarioActivities + type: object + properties: + activities: + type: array + items: + $ref: '#/components/schemas/ScenarioActivity' + ScenarioActivitiesMetadata: + title: ScenarioActivitiesMetadata + type: object + properties: + activities: + type: array + items: + $ref: '#/components/schemas/ScenarioActivityMetadata' + actions: + type: array + items: + $ref: '#/components/schemas/ScenarioActivityActionMetadata' ScenarioActivity: title: ScenarioActivity + oneOf: + - $ref: '#/components/schemas/AttachmentAdded' + - $ref: '#/components/schemas/AutomaticUpdate' + - $ref: '#/components/schemas/ChangedProcessingMode' + - $ref: '#/components/schemas/CommentAdded' + - $ref: '#/components/schemas/CustomAction' + - $ref: '#/components/schemas/IncomingMigration' + - $ref: '#/components/schemas/OutgoingMigration' + - $ref: '#/components/schemas/PerformedScheduledExecution' + - $ref: '#/components/schemas/PerformedSingleExecution' + - $ref: '#/components/schemas/ScenarioArchived' + - $ref: '#/components/schemas/ScenarioCanceled' + - $ref: '#/components/schemas/ScenarioCreated' + - $ref: '#/components/schemas/ScenarioDeployed' + - $ref: '#/components/schemas/ScenarioModified' + - $ref: '#/components/schemas/ScenarioNameChanged' + - $ref: '#/components/schemas/ScenarioPaused' + - $ref: '#/components/schemas/ScenarioUnarchived' + discriminator: + propertyName: type + mapping: + ATTACHMENT_ADDED: '#/components/schemas/AttachmentAdded' + AUTOMATIC_UPDATE: '#/components/schemas/AutomaticUpdate' + CHANGED_PROCESSING_MODE: '#/components/schemas/ChangedProcessingMode' + COMMENT_ADDED: '#/components/schemas/CommentAdded' + CUSTOM_ACTION: '#/components/schemas/CustomAction' + INCOMING_MIGRATION: '#/components/schemas/IncomingMigration' + OUTGOING_MIGRATION: '#/components/schemas/OutgoingMigration' + PERFORMED_SCHEDULED_EXECUTION: '#/components/schemas/PerformedScheduledExecution' + PERFORMED_SINGLE_EXECUTION: '#/components/schemas/PerformedSingleExecution' + SCENARIO_ARCHIVED: '#/components/schemas/ScenarioArchived' + SCENARIO_CANCELED: '#/components/schemas/ScenarioCanceled' + SCENARIO_CREATED: '#/components/schemas/ScenarioCreated' + SCENARIO_DEPLOYED: '#/components/schemas/ScenarioDeployed' + SCENARIO_MODIFIED: '#/components/schemas/ScenarioModified' + SCENARIO_NAME_CHANGED: '#/components/schemas/ScenarioNameChanged' + SCENARIO_PAUSED: '#/components/schemas/ScenarioPaused' + SCENARIO_UNARCHIVED: '#/components/schemas/ScenarioUnarchived' + ScenarioActivityActionMetadata: + title: ScenarioActivityActionMetadata type: object + required: + - id + - displayableName + - icon properties: - comments: + id: + type: string + displayableName: + type: string + icon: + type: string + ScenarioActivityAttachment: + title: ScenarioActivityAttachment + type: object + required: + - filename + - lastModifiedBy + - lastModifiedAt + properties: + id: + type: + - integer + - 'null' + format: int64 + filename: + type: string + lastModifiedBy: + type: string + lastModifiedAt: + type: string + format: date-time + ScenarioActivityComment: + title: ScenarioActivityComment + type: object + required: + - lastModifiedBy + - lastModifiedAt + properties: + comment: + type: + - string + - 'null' + lastModifiedBy: + type: string + lastModifiedAt: + type: string + format: date-time + ScenarioActivityMetadata: + title: ScenarioActivityMetadata + type: object + required: + - type + - displayableName + - icon + properties: + type: + type: string + displayableName: + type: string + icon: + type: string + supportedActions: type: array items: - $ref: '#/components/schemas/Comment' + type: string + ScenarioArchived: + title: ScenarioArchived + type: object + required: + - id + - user + - date + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + type: + type: string + ScenarioAttachments: + title: ScenarioAttachments + type: object + properties: attachments: type: array items: $ref: '#/components/schemas/Attachment' + ScenarioCanceled: + title: ScenarioCanceled + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string + ScenarioCreated: + title: ScenarioCreated + type: object + required: + - id + - user + - date + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + type: + type: string + ScenarioDeployed: + title: ScenarioDeployed + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string + ScenarioModified: + title: ScenarioModified + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string + ScenarioNameChanged: + title: ScenarioNameChanged + type: object + required: + - id + - user + - date + - oldName + - newName + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + oldName: + type: string + newName: + type: string + type: + type: string ScenarioParameters: title: ScenarioParameters type: object @@ -4453,6 +5984,57 @@ components: $ref: '#/components/schemas/ScenarioParameters' engineSetupErrors: $ref: '#/components/schemas/Map_EngineSetupName_List_String' + ScenarioPaused: + title: ScenarioPaused + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string + ScenarioUnarchived: + title: ScenarioUnarchived + type: object + required: + - id + - user + - date + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + type: + type: string ScenarioUsageData: title: ScenarioUsageData type: object From 20a1c6d7b8875614a0a00d62a93a83627fa60ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 9 Sep 2024 15:04:10 +0200 Subject: [PATCH 02/43] qs --- .../api/ScenarioActivityApiHttpService.scala | 78 +++++++++---------- 1 file changed, 39 insertions(+), 39 deletions(-) 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 486ad53dd3b..062c88c1f41 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 @@ -93,6 +93,31 @@ class ScenarioActivityApiHttpService( } } + expose { + endpoints.addAttachmentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: AddAttachmentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Write) + _ <- saveAttachment(request, scenarioId) + } yield () + } + } + + expose { + endpoints.downloadAttachmentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: GetAttachmentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Read) + maybeAttachment <- EitherT.right(attachmentService.readAttachment(request.attachmentId, scenarioId)) + response = buildResponse(maybeAttachment) + } yield response + } + } + expose { endpoints.scenarioActivitiesEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) @@ -100,11 +125,23 @@ class ScenarioActivityApiHttpService( for { scenarioId <- getScenarioIdByName(scenarioName) _ <- isAuthorized(scenarioId, Permission.Read) - activities <- EitherT.liftF(Future.failed(new Exception("API not yet implemented"))) + activities <- notImplemented[List[ScenarioActivity]] } yield ScenarioActivities(activities) } } + expose { + endpoints.attachmentsEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => processName: ProcessName => + for { + scenarioId <- getScenarioIdByName(processName) + _ <- isAuthorized(scenarioId, Permission.Read) + attachments <- notImplemented[ScenarioAttachments] + } yield attachments + } + } + expose { endpoints.scenarioActivitiesMetadataEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) @@ -124,7 +161,7 @@ class ScenarioActivityApiHttpService( for { scenarioId <- getScenarioIdByName(request.scenarioName) _ <- isAuthorized(scenarioId, Permission.Write) - _ <- addNewComment(request, scenarioId) + _ <- notImplemented[Unit] } yield () } } @@ -153,43 +190,6 @@ class ScenarioActivityApiHttpService( } } - expose { - endpoints.attachmentsEndpoint - .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) - .serverLogicEitherT { implicit loggedUser => processName: ProcessName => - for { - scenarioId <- getScenarioIdByName(processName) - _ <- isAuthorized(scenarioId, Permission.Read) - attachments <- notImplemented[ScenarioAttachments] - } yield attachments - } - } - - expose { - endpoints.addAttachmentEndpoint - .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) - .serverLogicEitherT { implicit loggedUser => request: AddAttachmentRequest => - for { - scenarioId <- getScenarioIdByName(request.scenarioName) - _ <- isAuthorized(scenarioId, Permission.Write) - _ <- saveAttachment(request, scenarioId) - } yield () - } - } - - expose { - endpoints.downloadAttachmentEndpoint - .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) - .serverLogicEitherT { implicit loggedUser => request: GetAttachmentRequest => - for { - scenarioId <- getScenarioIdByName(request.scenarioName) - _ <- isAuthorized(scenarioId, Permission.Read) - maybeAttachment <- EitherT.right(attachmentService.readAttachment(request.attachmentId, scenarioId)) - response = buildResponse(maybeAttachment) - } yield response - } - } - private def notImplemented[T]: EitherT[Future, ScenarioActivityError, T] = EitherT.leftT[Future, T](ScenarioActivityError.NotImplemented: ScenarioActivityError) From ae8b5b2b7fbc85fdea204656971bd2a0ed8690b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Thu, 5 Sep 2024 18:43:50 +0200 Subject: [PATCH 03/43] Test fixes, activities handling improvements --- ...__CreateScenarioActivitiesDefinition.scala | 104 +++ .../V1_055__CreateScenarioActivities.scala | 8 + .../V1_055__CreateScenarioActivities.scala | 8 + .../nussknacker/ui/api/BaseHttpService.scala | 6 +- .../ui/api/ProcessesExportResources.scala | 238 +++-- .../api/ScenarioActivityApiHttpService.scala | 309 +++++- .../touk/nussknacker/ui/api/TapirCodecs.scala | 13 +- .../ScenarioActivityApiEndpoints.scala | 280 ------ .../description/scenarioActivity/Dtos.scala | 752 +++++++++++++++ .../scenarioActivity/Endpoints.scala | 136 +++ .../scenarioActivity/Examples.scala | 217 +++++ .../scenarioActivity/InputOutput.scala | 26 + .../pl/touk/nussknacker/ui/db/NuTables.scala | 3 +- .../ui/db/entity/CommentEntityFactory.scala | 56 -- .../entity/ProcessActionEntityFactory.scala | 104 --- .../ScenarioActivityEntityFactory.scala | 165 ++++ .../ui/initialization/Initialization.scala | 8 +- .../notifications/NotificationService.scala | 74 +- .../process/ScenarioAttachmentService.scala | 34 +- ...cessingTypeDeployedScenariosProvider.scala | 3 +- .../deployment/DeploymentService.scala | 9 +- .../process/newactivity/ActivityService.scala | 40 +- .../newdeployment/DeploymentService.scala | 2 +- .../repository/CommentRepository.scala | 47 - .../DbProcessActivityRepository.scala | 174 ---- .../repository/DeploymentComment.scala | 38 +- .../repository/ProcessActionRepository.scala | 340 ++++--- .../repository/ProcessRepository.scala | 61 +- .../DbScenarioActivityRepository.scala | 647 +++++++++++++ .../ScenarioActivityRepository.scala | 63 ++ .../server/AkkaHttpBasedRouteProvider.scala | 40 +- ...sageStatisticsReportsSettingsService.scala | 10 +- .../nussknacker/ui/util/PdfExporter.scala | 783 ++++++++-------- .../test/base/it/NuResourcesTest.scala | 12 +- .../test/utils/domain/ScenarioHelper.scala | 10 +- .../test/utils/domain/TestFactory.scala | 10 +- .../ui/api/DeploymentCommentSpec.scala | 5 +- .../ui/api/ManagementResourcesSpec.scala | 8 +- ...DesignerApiAvailableToExposeYamlSpec.scala | 36 +- .../ProcessesExportImportResourcesSpec.scala | 347 +++---- .../ui/api/ProcessesResourcesSpec.scala | 6 +- .../InitializationOnDbItSpec.scala | 10 +- .../NotificationServiceTest.scala | 2 - .../ScenarioAttachmentServiceSpec.scala | 56 +- .../deployment/DeploymentServiceSpec.scala | 14 +- .../DBFetchingProcessRepositorySpec.scala | 26 +- .../nussknacker/ui/util/PdfExporterSpec.scala | 195 ++-- docs-internal/api/nu-designer-openapi.yaml | 880 ++++++++++++++---- .../api/deployment/ProcessActivity.scala | 234 +++++ 49 files changed, 4649 insertions(+), 2000 deletions(-) create mode 100644 designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala create mode 100644 designer/server/src/main/scala/db/migration/hsql/V1_055__CreateScenarioActivities.scala create mode 100644 designer/server/src/main/scala/db/migration/postgres/V1_055__CreateScenarioActivities.scala delete mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ScenarioActivityApiEndpoints.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Dtos.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Endpoints.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Examples.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/InputOutput.scala delete mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/CommentEntityFactory.scala delete mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/ProcessActionEntityFactory.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/ScenarioActivityEntityFactory.scala delete mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/CommentRepository.scala delete mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/DbProcessActivityRepository.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/activities/DbScenarioActivityRepository.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/activities/ScenarioActivityRepository.scala create mode 100644 extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala diff --git a/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala new file mode 100644 index 00000000000..d0a013bde34 --- /dev/null +++ b/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala @@ -0,0 +1,104 @@ +package db.migration + +import pl.touk.nussknacker.ui.db.migration.SlickMigration +import slick.sql.SqlProfile.ColumnOption.NotNull + +import java.sql.Timestamp +import java.util.UUID + +trait V1_055__CreateScenarioActivitiesDefinition extends SlickMigration { + + import profile.api._ + + override def migrateActions: DBIOAction[Any, NoStream, _ <: Effect] = { + scenarioActivitiesTable.schema.create + } + + private val scenarioActivitiesTable = TableQuery[ScenarioActivityEntity] + + private 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) + + def userId: Rep[String] = column[String]("user_id", NotNull) + + 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 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) + + override def * = + ( + id, + activityType, + scenarioId, + activityId, + userId, + userName, + impersonatedByUserId, + impersonatedByUserName, + lastModifiedByUserName, + createdAt, + scenarioVersion, + comment, + attachmentId, + performedAt, + state, + errorMessage, + buildInfo, + additionalProperties, + ) <> ( + ScenarioActivityEntityData.apply _ tupled, ScenarioActivityEntityData.unapply + ) + + } + + private sealed case class ScenarioActivityEntityData( + id: Long, + activityType: String, + scenarioId: Long, + activityId: UUID, + userId: String, + userName: String, + impersonatedByUserId: Option[String], + impersonatedByUserName: Option[String], + lastModifiedByUserName: Option[String], + 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/hsql/V1_055__CreateScenarioActivities.scala b/designer/server/src/main/scala/db/migration/hsql/V1_055__CreateScenarioActivities.scala new file mode 100644 index 00000000000..a21fbec32d9 --- /dev/null +++ b/designer/server/src/main/scala/db/migration/hsql/V1_055__CreateScenarioActivities.scala @@ -0,0 +1,8 @@ +package db.migration.hsql + +import db.migration.V1_055__CreateScenarioActivitiesDefinition +import slick.jdbc.{HsqldbProfile, JdbcProfile} + +class V1_055__CreateScenarioActivities extends V1_055__CreateScenarioActivitiesDefinition { + override protected lazy val profile: JdbcProfile = HsqldbProfile +} diff --git a/designer/server/src/main/scala/db/migration/postgres/V1_055__CreateScenarioActivities.scala b/designer/server/src/main/scala/db/migration/postgres/V1_055__CreateScenarioActivities.scala new file mode 100644 index 00000000000..d466e9f87bb --- /dev/null +++ b/designer/server/src/main/scala/db/migration/postgres/V1_055__CreateScenarioActivities.scala @@ -0,0 +1,8 @@ +package db.migration.postgres + +import db.migration.V1_055__CreateScenarioActivitiesDefinition +import slick.jdbc.{JdbcProfile, PostgresProfile} + +class V1_055__CreateScenarioActivities extends V1_055__CreateScenarioActivitiesDefinition { + 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 1dceac644a7..61376e8ace4 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 @@ -1,120 +1,118 @@ -package pl.touk.nussknacker.ui.api - -import akka.http.scaladsl.model._ -import akka.http.scaladsl.server.{Directives, Route} -import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} -import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport -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.process.ProcessService -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.{ - FetchingProcessRepository, - ProcessActivityRepository, - ScenarioWithDetailsEntity -} -import pl.touk.nussknacker.ui.security.api.LoggedUser -import pl.touk.nussknacker.ui.uiresolving.UIProcessResolver -import pl.touk.nussknacker.ui.util._ - -import scala.concurrent.{ExecutionContext, Future} - -class ProcessesExportResources( - processRepository: FetchingProcessRepository[Future], - protected val processService: ProcessService, - processActivityRepository: ProcessActivityRepository, - processResolvers: ProcessingTypeDataProvider[UIProcessResolver, _] -)(implicit val ec: ExecutionContext) - extends Directives - with FailFastCirceSupport - with RouteWithUser - with ProcessDirectives - with NuPathMatchers { - - private implicit final val string: FromEntityUnmarshaller[String] = - Unmarshaller.stringUnmarshaller.forContentTypes(ContentTypeRange.*) - - def securedRoute(implicit user: LoggedUser): Route = { - path("processesExport" / ProcessNameSegment) { processName => - (get & processId(processName)) { processId => - complete { - processRepository.fetchLatestProcessDetailsForProcessId[ScenarioGraph](processId.id).map { - exportProcess - } - } - } ~ (post & processDetailsForName(processName)) { processDetails => - entity(as[ScenarioGraph]) { process => - complete { - exportResolvedProcess( - process, - processDetails.processingType, - processDetails.name, - processDetails.isFragment - ) - } - } - } - } ~ path("processesExport" / ProcessNameSegment / VersionIdSegment) { (processName, versionId) => - (get & processId(processName)) { processId => - complete { - processRepository.fetchProcessDetailsForId[ScenarioGraph](processId.id, versionId).map { - exportProcess - } - } - } - } ~ path("processesExport" / "pdf" / ProcessNameSegment / VersionIdSegment) { (processName, versionId) => - (post & processId(processName)) { processId => - entity(as[String]) { svg => - complete { - processRepository.fetchProcessDetailsForId[ScenarioGraph](processId.id, versionId).flatMap { process => - processActivityRepository.findActivity(processId.id).map(exportProcessToPdf(svg, process, _)) - } - } - } - } - } - } - - private def exportProcess(processDetails: Option[ScenarioWithDetailsEntity[ScenarioGraph]]): HttpResponse = - processDetails.map(details => exportProcess(details.json, details.name)).getOrElse { - HttpResponse(status = StatusCodes.NotFound, entity = "Scenario not found") - } - - private def exportProcess(processDetails: ScenarioGraph, name: ProcessName): HttpResponse = { - fileResponse(CanonicalProcessConverter.fromScenarioGraph(processDetails, name)) - } - - private def exportResolvedProcess( - processWithDictLabels: ScenarioGraph, - processingType: ProcessingType, - processName: ProcessName, - isFragment: Boolean - )(implicit user: LoggedUser): HttpResponse = { - val processResolver = processResolvers.forProcessingTypeUnsafe(processingType) - val resolvedProcess = processResolver.validateAndResolve(processWithDictLabels, processName, isFragment) - fileResponse(resolvedProcess) - } - - private def fileResponse(canonicalProcess: CanonicalProcess) = { - val canonicalJson = canonicalProcess.asJson.spaces2 - val entity = HttpEntity(ContentTypes.`application/json`, canonicalJson) - AkkaHttpResponse.asFile(entity, s"${canonicalProcess.name}.json") - } - - private def exportProcessToPdf( - svg: String, - processDetails: Option[ScenarioWithDetailsEntity[ScenarioGraph]], - processActivity: ProcessActivity - ) = processDetails match { - case Some(process) => - val pdf = PdfExporter.exportToPdf(svg, process, processActivity) - HttpResponse(status = StatusCodes.OK, entity = HttpEntity(pdf)) - case None => - HttpResponse(status = StatusCodes.NotFound, entity = "Scenario not found") - } - -} +//package pl.touk.nussknacker.ui.api +// +//import akka.http.scaladsl.model._ +//import akka.http.scaladsl.server.{Directives, Route} +//import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} +//import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport +//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.process.ProcessService +//import pl.touk.nussknacker.ui.process.marshall.CanonicalProcessConverter +//import pl.touk.nussknacker.ui.process.processingtype.ProcessingTypeDataProvider +//import pl.touk.nussknacker.ui.process.repository.{ +// FetchingProcessRepository, +// ScenarioWithDetailsEntity +//} +//import pl.touk.nussknacker.ui.security.api.LoggedUser +//import pl.touk.nussknacker.ui.uiresolving.UIProcessResolver +//import pl.touk.nussknacker.ui.util._ +// +//import scala.concurrent.{ExecutionContext, Future} +// +//class ProcessesExportResources( +// processRepository: FetchingProcessRepository[Future], +// protected val processService: ProcessService, +// processActivityRepository: ProcessActivityRepository, +// processResolvers: ProcessingTypeDataProvider[UIProcessResolver, _] +//)(implicit val ec: ExecutionContext) +// extends Directives +// with FailFastCirceSupport +// with RouteWithUser +// with ProcessDirectives +// with NuPathMatchers { +// +// private implicit final val string: FromEntityUnmarshaller[String] = +// Unmarshaller.stringUnmarshaller.forContentTypes(ContentTypeRange.*) +// +// def securedRoute(implicit user: LoggedUser): Route = { +// path("processesExport" / ProcessNameSegment) { processName => +// (get & processId(processName)) { processId => +// complete { +// processRepository.fetchLatestProcessDetailsForProcessId[ScenarioGraph](processId.id).map { +// exportProcess +// } +// } +// } ~ (post & processDetailsForName(processName)) { processDetails => +// entity(as[ScenarioGraph]) { process => +// complete { +// exportResolvedProcess( +// process, +// processDetails.processingType, +// processDetails.name, +// processDetails.isFragment +// ) +// } +// } +// } +// } ~ path("processesExport" / ProcessNameSegment / VersionIdSegment) { (processName, versionId) => +// (get & processId(processName)) { processId => +// complete { +// processRepository.fetchProcessDetailsForId[ScenarioGraph](processId.id, versionId).map { +// exportProcess +// } +// } +// } +// } ~ path("processesExport" / "pdf" / ProcessNameSegment / VersionIdSegment) { (processName, versionId) => +// (post & processId(processName)) { processId => +// entity(as[String]) { svg => +// complete { +// processRepository.fetchProcessDetailsForId[ScenarioGraph](processId.id, versionId).flatMap { process => +// processActivityRepository.findActivity(processId.id).map(exportProcessToPdf(svg, process, _)) +// } +// } +// } +// } +// } +// } +// +// private def exportProcess(processDetails: Option[ScenarioWithDetailsEntity[ScenarioGraph]]): HttpResponse = +// processDetails.map(details => exportProcess(details.json, details.name)).getOrElse { +// HttpResponse(status = StatusCodes.NotFound, entity = "Scenario not found") +// } +// +// private def exportProcess(processDetails: ScenarioGraph, name: ProcessName): HttpResponse = { +// fileResponse(CanonicalProcessConverter.fromScenarioGraph(processDetails, name)) +// } +// +// private def exportResolvedProcess( +// processWithDictLabels: ScenarioGraph, +// processingType: ProcessingType, +// processName: ProcessName, +// isFragment: Boolean +// )(implicit user: LoggedUser): HttpResponse = { +// val processResolver = processResolvers.forProcessingTypeUnsafe(processingType) +// val resolvedProcess = processResolver.validateAndResolve(processWithDictLabels, processName, isFragment) +// fileResponse(resolvedProcess) +// } +// +// private def fileResponse(canonicalProcess: CanonicalProcess) = { +// val canonicalJson = canonicalProcess.asJson.spaces2 +// val entity = HttpEntity(ContentTypes.`application/json`, canonicalJson) +// AkkaHttpResponse.asFile(entity, s"${canonicalProcess.name}.json") +// } +// +// private def exportProcessToPdf( +// svg: String, +// processDetails: Option[ScenarioWithDetailsEntity[ScenarioGraph]], +// processActivity: ProcessActivity +// ) = processDetails match { +// case Some(process) => +// val pdf = PdfExporter.exportToPdf(svg, process, processActivity) +// HttpResponse(status = StatusCodes.OK, entity = HttpEntity(pdf)) +// case None => +// HttpResponse(status = StatusCodes.NotFound, entity = "Scenario not found") +// } +// +//} 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 8a9e1112a0b..8257481d7ea 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,24 @@ 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.ScenarioActivityApiEndpoints -import pl.touk.nussknacker.ui.api.description.ScenarioActivityApiEndpoints.Dtos.ScenarioActivityError.{ +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError.{ NoComment, NoPermission, NoScenario } -import pl.touk.nussknacker.ui.api.description.ScenarioActivityApiEndpoints.Dtos._ -import pl.touk.nussknacker.ui.process.repository.{ProcessActivityRepository, UserComment} +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos._ +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 @@ -21,37 +28,39 @@ import sttp.model.MediaType import java.io.ByteArrayInputStream import java.net.URLConnection +import java.time.Instant import scala.concurrent.{ExecutionContext, Future} class ScenarioActivityApiHttpService( authManager: AuthManager, - scenarioActivityRepository: ProcessActivityRepository, + scenarioActivityRepository: ScenarioActivityRepository, scenarioService: ProcessService, scenarioAuthorizer: AuthorizeProcess, attachmentService: ScenarioAttachmentService, - streamEndpointProvider: TapirStreamEndpointProvider + streamEndpointProvider: TapirStreamEndpointProvider, + dbioActionRunner: DBIOActionRunner, )(implicit executionContext: ExecutionContext) extends BaseHttpService(authManager) with LazyLogging { - private val scenarioActivityApiEndpoints = new ScenarioActivityApiEndpoints( - authManager.authenticationEndpointInput() - ) + private val securityInput = authManager.authenticationEndpointInput() + + private val endpoints = new Endpoints(securityInput, streamEndpointProvider) expose { - scenarioActivityApiEndpoints.scenarioActivityEndpoint + endpoints.scenarioActivitiesEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => scenarioName: ProcessName => for { - scenarioId <- getScenarioIdByName(scenarioName) - _ <- isAuthorized(scenarioId, Permission.Read) - scenarioActivity <- EitherT.right(scenarioActivityRepository.findActivity(scenarioId)) - } yield ScenarioActivity(scenarioActivity) + scenarioId <- getScenarioIdByName(scenarioName) + _ <- isAuthorized(scenarioId, Permission.Write) + activities <- fetchActivities(scenarioId) + } yield ScenarioActivities(activities) } } expose { - scenarioActivityApiEndpoints.addCommentEndpoint + endpoints.addCommentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => request: AddCommentRequest => for { @@ -63,7 +72,19 @@ class ScenarioActivityApiHttpService( } expose { - scenarioActivityApiEndpoints.deleteCommentEndpoint + endpoints.editCommentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: EditCommentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Write) + _ <- editComment(request) + } yield () + } + } + + expose { + endpoints.deleteCommentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => request: DeleteCommentRequest => for { @@ -75,8 +96,7 @@ class ScenarioActivityApiHttpService( } expose { - scenarioActivityApiEndpoints - .addAttachmentEndpoint(streamEndpointProvider.streamBodyEndpointInput) + endpoints.addAttachmentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => request: AddAttachmentRequest => for { @@ -88,8 +108,7 @@ class ScenarioActivityApiHttpService( } expose { - scenarioActivityApiEndpoints - .downloadAttachmentEndpoint(streamEndpointProvider.streamBodyEndpointOutput) + endpoints.downloadAttachmentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => request: GetAttachmentRequest => for { @@ -120,17 +139,257 @@ class ScenarioActivityApiHttpService( } ) + private def fetchActivities(scenarioId: ProcessId)( + implicit loggedUser: LoggedUser + ): EitherT[Future, ScenarioActivityError, List[Dtos.ScenarioActivity]] = + EitherT + .right( + dbioActionRunner.run( + scenarioActivityRepository.fetchActivities(scenarioId) + ) + ) + .map(_.map(toDto).toList) + + private def toDto(scenarioComment: ScenarioComment): Dtos.ScenarioActivityComment = { + scenarioComment match { + case ScenarioComment.Available(comment, lastModifiedByUserName) => + Dtos.ScenarioActivityComment( + comment = Some(comment), + lastModifiedBy = lastModifiedByUserName.value, + lastModifiedAt = Instant.now(), + ) + case ScenarioComment.Deleted(deletedByUserName) => + Dtos.ScenarioActivityComment( + comment = None, + lastModifiedBy = deletedByUserName.value, + lastModifiedAt = Instant.now(), + ) + } + } + + private def toDto(attachment: ScenarioAttachment): Dtos.ScenarioActivityAttachment = { + attachment match { + case ScenarioAttachment.Available(attachmentId, attachmentFilename, lastModifiedByUserName) => + Dtos.ScenarioActivityAttachment( + id = Some(attachmentId.value), + filename = attachmentFilename.value, + lastModifiedBy = lastModifiedByUserName.value, + lastModifiedAt = Instant.now() + ) + case ScenarioAttachment.Deleted(attachmentFilename, deletedByUserName) => + Dtos.ScenarioActivityAttachment( + id = None, + filename = attachmentFilename.value, + lastModifiedBy = deletedByUserName.value, + lastModifiedAt = Instant.now() + ) + } + } + + 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, comment, oldName, newName) => + Dtos.ScenarioActivity.ScenarioNameChanged( + id = id.value, + user = user.name.value, + date = date, + scenarioVersion = version.map(_.value), + comment = toDto(comment), + 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, + dateFinished, + errorMessage + ) => + Dtos.ScenarioActivity.PerformedSingleExecution( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value), + 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, + ) + } + } + 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: DeleteCommentRequest): EitherT[Future, ScenarioActivityError, Unit] = + private def editComment(request: EditCommentRequest)( + implicit loggedUser: LoggedUser + ): EitherT[Future, ScenarioActivityError, Unit] = + EitherT( + dbioActionRunner.run( + scenarioActivityRepository.editComment(ScenarioActivityId(request.scenarioActivityId), request.commentContent) + ) + ).leftMap(_ => NoComment(request.scenarioActivityId.toString)) + + private def deleteComment(request: DeleteCommentRequest)( + implicit loggedUser: LoggedUser + ): EitherT[Future, ScenarioActivityError, Unit] = EitherT( - scenarioActivityRepository.deleteComment(request.commentId) - ).leftMap(_ => NoComment(request.commentId)) + dbioActionRunner.run(scenarioActivityRepository.deleteComment(ScenarioActivityId(request.scenarioActivityId))) + ).leftMap(_ => NoComment(request.scenarioActivityId.toString)) private def saveAttachment(request: AddAttachmentRequest, scenarioId: ProcessId)( implicit loggedUser: LoggedUser diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TapirCodecs.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TapirCodecs.scala index 5cf51602f58..52fbab1403d 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TapirCodecs.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TapirCodecs.scala @@ -9,7 +9,7 @@ import pl.touk.nussknacker.engine.deployment.EngineSetupName import pl.touk.nussknacker.ui.server.HeadersSupport.{ContentDisposition, FileName} import sttp.tapir.Codec.PlainCodec import sttp.tapir.CodecFormat.TextPlain -import sttp.tapir.{Codec, CodecFormat, DecodeResult, Schema} +import sttp.tapir.{Codec, CodecFormat, DecodeResult, Schema, Validator} import java.net.URL @@ -133,4 +133,15 @@ object TapirCodecs { implicit val classSchema: Schema[Class[_]] = Schema.string[Class[_]] } + def enumSchema[T]( + items: List[T], + encoder: T => String, + ): Schema[T] = + Schema.string.validate( + Validator.enumeration( + items, + (i: T) => Some(encoder(i)), + ), + ) + } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ScenarioActivityApiEndpoints.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ScenarioActivityApiEndpoints.scala deleted file mode 100644 index db36ba1b867..00000000000 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ScenarioActivityApiEndpoints.scala +++ /dev/null @@ -1,280 +0,0 @@ -package pl.touk.nussknacker.ui.api.description - -import derevo.circe.{decoder, encoder} -import derevo.derive -import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} -import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions -import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions.SecuredEndpoint -import pl.touk.nussknacker.security.AuthCredentials -import pl.touk.nussknacker.ui.api.BaseHttpService.CustomAuthorizationError -import pl.touk.nussknacker.ui.api.TapirCodecs -import pl.touk.nussknacker.ui.process.repository.DbProcessActivityRepository.{ - Attachment => DbAttachment, - Comment => DbComment, - ProcessActivity => DbProcessActivity -} -import pl.touk.nussknacker.ui.server.HeadersSupport.FileName -import sttp.model.StatusCode.{InternalServerError, NotFound, Ok} -import sttp.model.{HeaderNames, MediaType} -import sttp.tapir.EndpointIO.Example -import sttp.tapir._ -import sttp.tapir.derevo.schema -import sttp.tapir.json.circe.jsonBody - -import java.io.InputStream -import java.time.Instant - -class ScenarioActivityApiEndpoints(auth: EndpointInput[AuthCredentials]) extends BaseEndpointDefinitions { - - import ScenarioActivityApiEndpoints.Dtos.ScenarioActivityError._ - import ScenarioActivityApiEndpoints.Dtos._ - import TapirCodecs.ContentDispositionCodec._ - import TapirCodecs.HeaderCodec._ - import TapirCodecs.ScenarioNameCodec._ - import TapirCodecs.VersionIdCodec._ - - lazy val scenarioActivityEndpoint: SecuredEndpoint[ProcessName, ScenarioActivityError, ScenarioActivity, Any] = - baseNuApiEndpoint - .summary("Scenario activity service") - .tag("Scenario") - .get - .in("processes" / path[ProcessName]("scenarioName") / "activity") - .out( - statusCode(Ok).and( - jsonBody[ScenarioActivity].example( - Example.of( - summary = Some("Display scenario activity"), - value = ScenarioActivity( - comments = List( - Comment( - id = 1L, - processVersionId = 1L, - content = "some comment", - user = "test", - createDate = Instant.parse("2024-01-17T14:21:17Z") - ) - ), - attachments = List( - Attachment( - id = 1L, - processVersionId = 1L, - fileName = "some_file.txt", - user = "test", - createDate = Instant.parse("2024-01-17T14:21:17Z") - ) - ) - ) - ) - ) - ) - ) - .errorOut(scenarioNotFoundErrorOutput) - .withSecurity(auth) - - lazy val addCommentEndpoint: SecuredEndpoint[AddCommentRequest, ScenarioActivityError, Unit, Any] = - baseNuApiEndpoint - .summary("Add scenario comment service") - .tag("Scenario") - .post - .in( - ("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" - / "comments" / stringBody).mapTo[AddCommentRequest] - ) - .out(statusCode(Ok)) - .errorOut(scenarioNotFoundErrorOutput) - .withSecurity(auth) - - lazy val deleteCommentEndpoint: SecuredEndpoint[DeleteCommentRequest, ScenarioActivityError, Unit, Any] = - baseNuApiEndpoint - .summary("Delete process comment service") - .tag("Scenario") - .delete - .in( - ("processes" / path[ProcessName]("scenarioName") / "activity" / "comments" - / path[Long]("commentId")).mapTo[DeleteCommentRequest] - ) - .out(statusCode(Ok)) - .errorOut( - oneOf[ScenarioActivityError]( - oneOfVariantFromMatchType( - NotFound, - plainBody[NoScenario] - .example( - Example.of( - summary = Some("No scenario {scenarioName} found"), - value = NoScenario(ProcessName("'example scenario'")) - ) - ) - ), - oneOfVariantFromMatchType( - InternalServerError, - plainBody[NoComment] - .example( - Example.of( - summary = Some("Unable to delete comment with id: {commentId}"), - value = NoComment(1L) - ) - ) - ) - ) - ) - .withSecurity(auth) - - def addAttachmentEndpoint( - implicit streamBodyEndpoint: EndpointInput[InputStream] - ): SecuredEndpoint[AddAttachmentRequest, ScenarioActivityError, Unit, Any] = { - baseNuApiEndpoint - .summary("Add scenario attachment service") - .tag("Scenario") - .post - .in( - ( - "processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" - / "attachments" / streamBodyEndpoint / header[FileName](HeaderNames.ContentDisposition) - ).mapTo[AddAttachmentRequest] - ) - .out(statusCode(Ok)) - .errorOut(scenarioNotFoundErrorOutput) - .withSecurity(auth) - } - - def downloadAttachmentEndpoint( - implicit streamBodyEndpoint: EndpointOutput[InputStream] - ): SecuredEndpoint[GetAttachmentRequest, ScenarioActivityError, GetAttachmentResponse, Any] = { - baseNuApiEndpoint - .summary("Download attachment service") - .tag("Scenario") - .get - .in( - ("processes" / path[ProcessName]("processName") / "activity" / "attachments" - / path[Long]("attachmentId")).mapTo[GetAttachmentRequest] - ) - .out( - statusCode(Ok) - .and(streamBodyEndpoint) - .and(header(HeaderNames.ContentDisposition)(optionalHeaderCodec)) - .and(header(HeaderNames.ContentType)(requiredHeaderCodec)) - .mapTo[GetAttachmentResponse] - ) - .errorOut(scenarioNotFoundErrorOutput) - .withSecurity(auth) - } - - private lazy val scenarioNotFoundErrorOutput: EndpointOutput.OneOf[ScenarioActivityError, ScenarioActivityError] = - oneOf[ScenarioActivityError]( - oneOfVariantFromMatchType( - NotFound, - plainBody[NoScenario] - .example( - Example.of( - summary = Some("No scenario {scenarioName} found"), - value = NoScenario(ProcessName("'example scenario'")) - ) - ) - ) - ) - -} - -object ScenarioActivityApiEndpoints { - - object Dtos { - @derive(encoder, decoder, schema) - final case class ScenarioActivity private (comments: List[Comment], attachments: List[Attachment]) - - object ScenarioActivity { - - def apply(activity: DbProcessActivity): ScenarioActivity = - new ScenarioActivity( - comments = activity.comments.map(Comment.apply), - attachments = activity.attachments.map(Attachment.apply) - ) - - } - - @derive(encoder, decoder, schema) - final case class Comment private ( - id: Long, - processVersionId: Long, - content: String, - user: String, - createDate: Instant - ) - - object Comment { - - def apply(comment: DbComment): Comment = - new Comment( - id = comment.id, - processVersionId = comment.processVersionId.value, - content = comment.content, - user = comment.user, - createDate = comment.createDate - ) - - } - - @derive(encoder, decoder, schema) - final case class Attachment private ( - id: Long, - processVersionId: Long, - fileName: String, - user: String, - createDate: Instant - ) - - object Attachment { - - def apply(attachment: DbAttachment): Attachment = - new Attachment( - id = attachment.id, - processVersionId = attachment.processVersionId.value, - fileName = attachment.fileName, - user = attachment.user, - createDate = attachment.createDate - ) - - } - - final case class AddCommentRequest(scenarioName: ProcessName, versionId: VersionId, commentContent: String) - - final case class DeleteCommentRequest(scenarioName: ProcessName, commentId: Long) - - final case class AddAttachmentRequest( - scenarioName: ProcessName, - versionId: VersionId, - body: InputStream, - fileName: FileName - ) - - final case class GetAttachmentRequest(scenarioName: ProcessName, attachmentId: Long) - - final case class GetAttachmentResponse(inputStream: InputStream, fileName: Option[String], contentType: String) - - object GetAttachmentResponse { - val emptyResponse: GetAttachmentResponse = - GetAttachmentResponse(InputStream.nullInputStream(), None, MediaType.TextPlainUtf8.toString()) - } - - sealed trait ScenarioActivityError - - object ScenarioActivityError { - final case class NoScenario(scenarioName: ProcessName) extends ScenarioActivityError - final case object NoPermission extends ScenarioActivityError with CustomAuthorizationError - 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" - ) - - implicit val noCommentCodec: Codec[String, NoComment, CodecFormat.TextPlain] = - BaseEndpointDefinitions.toTextPlainCodecSerializationOnly[NoComment](e => - s"Unable to delete comment with id: ${e.commentId}" - ) - - } - - } - -} 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 new file mode 100644 index 00000000000..ee8d70ca1c8 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Dtos.scala @@ -0,0 +1,752 @@ +package pl.touk.nussknacker.ui.api.description.scenarioActivity + +import derevo.circe.{decoder, encoder} +import derevo.derive +import enumeratum.EnumEntry.UpperSnakecase +import enumeratum.{Enum, EnumEntry} +import io.circe +import io.circe.generic.extras.Configuration +import io.circe.generic.extras.semiauto.deriveConfiguredCodec +import io.circe.{Decoder, Encoder} +import pl.touk.nussknacker.engine.api.deployment.ScenarioVersion +import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} +import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions +import pl.touk.nussknacker.ui.api.BaseHttpService.CustomAuthorizationError +import pl.touk.nussknacker.ui.api.TapirCodecs.enumSchema +import pl.touk.nussknacker.ui.server.HeadersSupport.FileName +import sttp.model.MediaType +import sttp.tapir._ +import sttp.tapir.derevo.schema + +import java.io.InputStream +import java.time.Instant +import java.util.UUID +import scala.collection.immutable + +object Dtos { + + sealed trait ScenarioActivityType extends EnumEntry with UpperSnakecase { + def displayableName: String + def icon: String + def supportedActions: List[String] + } + + object ScenarioActivityType extends Enum[ScenarioActivityType] { + + case object ScenarioCreated extends ScenarioActivityType { + override def displayableName: String = "Scenario created" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioArchived extends ScenarioActivityType { + override def displayableName: String = "Scenario archived" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioUnarchived extends ScenarioActivityType { + override def displayableName: String = "Scenario unarchived" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioDeployed extends ScenarioActivityType { + override def displayableName: String = "Deployment" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioCanceled extends ScenarioActivityType { + override def displayableName: String = "Cancel" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioModified extends ScenarioActivityType { + override def displayableName: String = "New version saved" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List("compare") + } + + case object ScenarioNameChanged extends ScenarioActivityType { + override def displayableName: String = "Scenario name changed" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object CommentAdded extends ScenarioActivityType { + override def displayableName: String = "Comment" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List("delete_comment", "edit_comment") + } + + case object AttachmentAdded extends ScenarioActivityType { + override def displayableName: String = "Attachment" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ChangedProcessingMode extends ScenarioActivityType { + override def displayableName: String = "Processing mode change" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object IncomingMigration extends ScenarioActivityType { + override def displayableName: String = "Incoming migration" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List("compare") + } + + case object OutgoingMigration extends ScenarioActivityType { + override def displayableName: String = "Outgoing migration" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object PerformedSingleExecution extends ScenarioActivityType { + override def displayableName: String = "Processing data" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object PerformedScheduledExecution extends ScenarioActivityType { + override def displayableName: String = "Processing data" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object AutomaticUpdate extends ScenarioActivityType { + override def displayableName: String = "Automatic update" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List("compare") + } + + override def values: immutable.IndexedSeq[ScenarioActivityType] = findValues + + implicit def scenarioActivityTypeSchema: Schema[ScenarioActivityType] = + enumSchema[ScenarioActivityType]( + ScenarioActivityType.values.toList, + _.entryName, + ) + + implicit def scenarioActivityTypeCodec: circe.Codec[ScenarioActivityType] = circe.Codec.from( + Decoder.decodeString.emap(str => + ScenarioActivityType.withNameEither(str).left.map(_ => s"Invalid scenario action type [$str]") + ), + Encoder.encodeString.contramap(_.entryName), + ) + + implicit def scenarioActivityTypeTextCodec: Codec[String, ScenarioActivityType, CodecFormat.TextPlain] = + Codec.string.map( + Mapping.fromDecode[String, ScenarioActivityType] { + ScenarioActivityType.withNameOption(_) match { + case Some(value) => DecodeResult.Value(value) + case None => DecodeResult.InvalidValue(Nil) + } + }(_.entryName) + ) + + } + + @derive(encoder, decoder, schema) + final case class ScenarioActivityComment(comment: Option[String], lastModifiedBy: String, lastModifiedAt: Instant) + + @derive(encoder, decoder, schema) + final case class ScenarioActivityAttachment( + id: Option[Long], + filename: String, + lastModifiedBy: String, + lastModifiedAt: Instant + ) + + @derive(encoder, decoder, schema) + final case class ScenarioActivities(activities: List[ScenarioActivity]) + + @derive(schema) + sealed trait ScenarioActivity { + def id: UUID + def user: String + def date: Instant + def scenarioVersion: Option[Long] + } + + object ScenarioActivity { + + implicit val scenarioActivityCodec: circe.Codec[ScenarioActivity] = { + implicit val configuration: Configuration = + Configuration.default.withDiscriminator("type").withScreamingSnakeCaseConstructorNames + deriveConfiguredCodec + } + + final case class ScenarioCreated( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + ) extends ScenarioActivity + + final case class ScenarioArchived( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + ) extends ScenarioActivity + + final case class ScenarioUnarchived( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + ) extends ScenarioActivity + + // Scenario deployments + + final case class ScenarioDeployed( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + final case class ScenarioPaused( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + final case class ScenarioCanceled( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + // Scenario modifications + + final case class ScenarioModified( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + final case class ScenarioNameChanged( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + oldName: String, + newName: String, + ) extends ScenarioActivity + + final case class CommentAdded( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + final case class AttachmentAdded( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + attachment: ScenarioActivityAttachment, + ) extends ScenarioActivity + + final case class ChangedProcessingMode( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + from: String, + to: String, + ) extends ScenarioActivity + + // Migration between environments + + final case class IncomingMigration( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + sourceEnvironment: String, + sourceScenarioVersion: String, + ) extends ScenarioActivity + + final case class OutgoingMigration( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + destinationEnvironment: String, + ) extends ScenarioActivity + + // Batch + + final case class PerformedSingleExecution( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + dateFinished: Instant, + errorMessage: Option[String], + ) extends ScenarioActivity + + final case class PerformedScheduledExecution( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + dateFinished: Instant, + errorMessage: Option[String], + ) extends ScenarioActivity + + // Other/technical + + final case class AutomaticUpdate( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + dateFinished: Instant, + changes: String, + errorMessage: Option[String], + ) extends ScenarioActivity + + } + +// +// @derive(schema) +// final case class ScenarioActivity( +// id: ScenarioActivityId, +// `type`: ScenarioActivityType, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// additionalFields: List[AdditionalField], +// overrideDisplayableName: Option[String] = None, +// overrideSupportedActions: Option[List[String]] = None +// ) +// +// object ScenarioActivity { +// + +// +// @derive(encoder, decoder, schema) +// final case class AdditionalField( +// name: String, +// value: String +// ) +// +// def forScenarioCreated( +// id: ScenarioActivityId, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// ): ScenarioActivity = ScenarioActivity( +// id = id, +// `type` = ScenarioActivityType.ScenarioCreated, +// user = user, +// date = date, +// scenarioVersion = scenarioVersion, +// comment = comment, +// additionalFields = List.empty, +// ) +// +// def forScenarioArchived( +// id: ScenarioActivityId, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// ): ScenarioActivity = ScenarioActivity( +// id = id, +// `type` = ScenarioActivityType.ScenarioArchived, +// user = user, +// date = date, +// scenarioVersion = scenarioVersion, +// comment = comment, +// additionalFields = List.empty, +// ) +// +// def forScenarioUnarchived( +// id: ScenarioActivityId, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// ): ScenarioActivity = ScenarioActivity( +// id = id, +// `type` = ScenarioActivityType.ScenarioUnarchived, +// user = user, +// date = date, +// scenarioVersion = scenarioVersion, +// comment = comment, +// additionalFields = List.empty, +// ) +// +// // Scenario deployments +// +// def forScenarioDeployed( +// id: ScenarioActivityId, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// ): ScenarioActivity = ScenarioActivity( +// id = id, +// `type` = ScenarioActivityType.ScenarioDeployed, +// user = user, +// date = date, +// scenarioVersion = scenarioVersion, +// comment = comment, +// additionalFields = List.empty, +// ) +// +// def forScenarioCanceled( +// id: ScenarioActivityId, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// ): ScenarioActivity = ScenarioActivity( +// id = id, +// `type` = ScenarioActivityType.ScenarioCanceled, +// user = user, +// date = date, +// scenarioVersion = scenarioVersion, +// comment = comment, +// additionalFields = List.empty, +// ) +// +// // Scenario modifications +// +// def forScenarioModified( +// id: ScenarioActivityId, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// ): ScenarioActivity = ScenarioActivity( +// id = id, +// `type` = ScenarioActivityType.ScenarioModified, +// user = user, +// date = date, +// scenarioVersion = scenarioVersion, +// comment = comment, +// additionalFields = List.empty, +// overrideDisplayableName = Some(s"Version $scenarioVersion saved"), +// ) +// +// def forScenarioNameChanged( +// id: ScenarioActivityId, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// oldName: String, +// newName: String, +// ): ScenarioActivity = ScenarioActivity( +// id = id, +// `type` = ScenarioActivityType.ScenarioNameChanged, +// user = user, +// date = date, +// scenarioVersion = scenarioVersion, +// comment = comment, +// additionalFields = List( +// AdditionalField("oldName", oldName), +// AdditionalField("newName", newName), +// ) +// ) +// +// def forCommentAdded( +// id: ScenarioActivityId, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// ): ScenarioActivity = ScenarioActivity( +// id = id, +// `type` = ScenarioActivityType.CommentAdded, +// user = user, +// date = date, +// scenarioVersion = scenarioVersion, +// comment = comment, +// additionalFields = List.empty, +// ) +// +// def forCommentAddedAndDeleted( +// id: ScenarioActivityId, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// deletedByUser: String, +// ): ScenarioActivity = ScenarioActivity( +// id = id, +// `type` = ScenarioActivityType.CommentAdded, +// user = user, +// date = date, +// scenarioVersion = scenarioVersion, +// comment = comment, +// additionalFields = List( +// AdditionalField("deletedByUser", deletedByUser), +// ), +// overrideSupportedActions = Some(List.empty) +// ) +// +// def forAttachmentPresent( +// id: ScenarioActivityId, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// attachmentId: String, +// attachmentFilename: String, +// ): ScenarioActivity = ScenarioActivity( +// id = id, +// `type` = ScenarioActivityType.AttachmentAdded, +// user = user, +// date = date, +// scenarioVersion = scenarioVersion, +// comment = comment, +// additionalFields = List( +// AdditionalField("attachmentId", attachmentId), +// AdditionalField("attachmentFilename", attachmentFilename), +// ) +// ) +// +// def forAttachmentDeleted( +// id: ScenarioActivityId, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// deletedByUser: String, +// ): ScenarioActivity = ScenarioActivity( +// id = id, +// `type` = ScenarioActivityType.AttachmentAdded, +// user = user, +// date = date, +// scenarioVersion = scenarioVersion, +// comment = comment, +// additionalFields = List( +// AdditionalField("deletedByUser", deletedByUser), +// ), +// overrideSupportedActions = Some(List.empty) +// ) +// +// def forChangedProcessingMode( +// id: ScenarioActivityId, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// from: String, +// to: String, +// ): ScenarioActivity = ScenarioActivity( +// id = id, +// `type` = ScenarioActivityType.ChangedProcessingMode, +// user = user, +// date = date, +// scenarioVersion = scenarioVersion, +// comment = comment, +// additionalFields = List( +// AdditionalField("from", from), +// AdditionalField("to", to), +// ) +// ) +// +// // Migration between environments +// +// def forIncomingMigration( +// id: ScenarioActivityId, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// sourceEnvironment: String, +// sourceScenarioVersion: String, +// ): ScenarioActivity = ScenarioActivity( +// id = id, +// `type` = ScenarioActivityType.IncomingMigration, +// user = user, +// date = date, +// scenarioVersion = scenarioVersion, +// comment = comment, +// additionalFields = List( +// AdditionalField("sourceEnvironment", sourceEnvironment), +// AdditionalField("sourceScenarioVersion", sourceScenarioVersion), +// ) +// ) +// +// def forOutgoingMigration( +// id: ScenarioActivityId, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// destinationEnvironment: String, +// ): ScenarioActivity = ScenarioActivity( +// id = id, +// `type` = ScenarioActivityType.OutgoingMigration, +// user = user, +// date = date, +// scenarioVersion = scenarioVersion, +// comment = comment, +// additionalFields = List( +// AdditionalField("destinationEnvironment", destinationEnvironment), +// ) +// ) +// +// // Batch +// +// def forPerformedSingleExecution( +// id: ScenarioActivityId, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// dateFinished: String, +// status: String, +// ): ScenarioActivity = ScenarioActivity( +// id = id, +// `type` = ScenarioActivityType.PerformedSingleExecution, +// user = user, +// date = date, +// scenarioVersion = scenarioVersion, +// comment = comment, +// additionalFields = List( +// AdditionalField("dateFinished", dateFinished), +// AdditionalField("status", status), +// ) +// ) +// +// def forPerformedScheduledExecution( +// id: ScenarioActivityId, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// dateFinished: String, +// params: String, +// status: String, +// ): ScenarioActivity = ScenarioActivity( +// id = id, +// `type` = ScenarioActivityType.PerformedScheduledExecution, +// user = user, +// date = date, +// scenarioVersion = scenarioVersion, +// comment = comment, +// additionalFields = List( +// AdditionalField("params", params), +// AdditionalField("dateFinished", dateFinished), +// AdditionalField("status", status), +// ) +// ) +// +// // Other/technical +// +// def forAutomaticUpdate( +// id: ScenarioActivityId, +// user: String, +// date: Instant, +// scenarioVersion: Option[ScenarioVersion], +// comment: Option[String], +// dateFinished: String, +// changes: String, +// status: String, +// ): ScenarioActivity = ScenarioActivity( +// id = id, +// `type` = ScenarioActivityType.AutomaticUpdate, +// user = user, +// date = date, +// scenarioVersion = scenarioVersion, +// comment = comment, +// additionalFields = List( +// AdditionalField("changes", changes), +// AdditionalField("dateFinished", dateFinished), +// AdditionalField("status", status), +// ) +// ) +// +// } + + @derive(encoder, decoder, schema) + final case class ScenarioAttachments(attachments: List[Attachment]) + + @derive(encoder, decoder, schema) + final case class Comment private ( + id: Long, + scenarioVersion: Long, + content: String, + user: String, + createDate: Instant + ) + + @derive(encoder, decoder, schema) + final case class Attachment private ( + id: Long, + scenarioVersion: Long, + fileName: String, + user: String, + createDate: Instant + ) + + final case class AddCommentRequest(scenarioName: ProcessName, versionId: VersionId, commentContent: String) + + final case class EditCommentRequest( + scenarioName: ProcessName, + scenarioActivityId: UUID, + commentContent: String + ) + + final case class DeleteCommentRequest(scenarioName: ProcessName, scenarioActivityId: UUID) + + final case class AddAttachmentRequest( + scenarioName: ProcessName, + versionId: VersionId, + body: InputStream, + fileName: FileName + ) + + final case class GetAttachmentRequest(scenarioName: ProcessName, attachmentId: Long) + + final case class GetAttachmentResponse(inputStream: InputStream, fileName: Option[String], contentType: String) + + object GetAttachmentResponse { + val emptyResponse: GetAttachmentResponse = + GetAttachmentResponse(InputStream.nullInputStream(), None, MediaType.TextPlainUtf8.toString()) + } + + sealed trait ScenarioActivityError + + object ScenarioActivityError { + final case class NoScenario(scenarioName: ProcessName) extends ScenarioActivityError + final case object NoPermission extends ScenarioActivityError with CustomAuthorizationError + final case class NoComment(scenarioActivityId: String) extends ScenarioActivityError + + implicit val noScenarioCodec: Codec[String, NoScenario, CodecFormat.TextPlain] = + BaseEndpointDefinitions.toTextPlainCodecSerializationOnly[NoScenario](e => s"No scenario ${e.scenarioName} found") + + implicit val noCommentCodec: Codec[String, NoComment, CodecFormat.TextPlain] = + BaseEndpointDefinitions.toTextPlainCodecSerializationOnly[NoComment](e => + s"Unable to delete comment for activity with id: ${e.scenarioActivityId}" + ) + + } + +} 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 new file mode 100644 index 00000000000..6f6a7835a8d --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Endpoints.scala @@ -0,0 +1,136 @@ +package pl.touk.nussknacker.ui.api.description.scenarioActivity + +import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} +import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions +import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions.SecuredEndpoint +import pl.touk.nussknacker.security.AuthCredentials +import pl.touk.nussknacker.ui.api.TapirCodecs +import pl.touk.nussknacker.ui.server.HeadersSupport.FileName +import pl.touk.nussknacker.ui.server.TapirStreamEndpointProvider +import sttp.model.HeaderNames +import sttp.model.StatusCode.{InternalServerError, NotFound, Ok} +import sttp.tapir._ +import sttp.tapir.json.circe.jsonBody + +import java.util.UUID + +class Endpoints(auth: EndpointInput[AuthCredentials], streamProvider: TapirStreamEndpointProvider) + extends BaseEndpointDefinitions { + + import TapirCodecs.ContentDispositionCodec._ + import TapirCodecs.HeaderCodec._ + import TapirCodecs.ScenarioNameCodec._ + import TapirCodecs.VersionIdCodec._ + import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError._ + import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos._ + import pl.touk.nussknacker.ui.api.description.scenarioActivity.InputOutput._ + + lazy val scenarioActivitiesEndpoint: SecuredEndpoint[ + ProcessName, + ScenarioActivityError, + ScenarioActivities, + Any + ] = + baseNuApiEndpoint + .summary("Scenario activities service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity") + .out(statusCode(Ok).and(jsonBody[ScenarioActivities].example(Examples.scenarioActivities))) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + + lazy val addCommentEndpoint: SecuredEndpoint[AddCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Add scenario comment service") + .tag("Activities") + .post + .in("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" / "comments") + .in(stringBody) + .mapInTo[AddCommentRequest] + .out(statusCode(Ok)) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + + lazy val editCommentEndpoint: SecuredEndpoint[EditCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Edit process comment service") + .tag("Activities") + .put + .in( + "processes" / path[ProcessName]("scenarioName") / "activity" / "comments" / path[UUID]("scenarioActivityId") + ) + .in(stringBody) + .mapInTo[EditCommentRequest] + .out(statusCode(Ok)) + .errorOut( + oneOf[ScenarioActivityError]( + oneOfVariantFromMatchType(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), + oneOfVariantFromMatchType(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)) + ) + ) + .withSecurity(auth) + + lazy val deleteCommentEndpoint: SecuredEndpoint[DeleteCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Delete process comment service") + .tag("Activities") + .delete + .in( + "processes" / path[ProcessName]("scenarioName") / "activity" / "comments" / path[UUID]("scenarioActivityId") + ) + .mapInTo[DeleteCommentRequest] + .out(statusCode(Ok)) + .errorOut( + oneOf[ScenarioActivityError]( + oneOfVariantFromMatchType(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), + oneOfVariantFromMatchType(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)) + ) + ) + .withSecurity(auth) + + val attachmentsEndpoint: SecuredEndpoint[ProcessName, ScenarioActivityError, ScenarioAttachments, Any] = { + baseNuApiEndpoint + .summary("Scenario attachments service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity" / "attachments") + .out(statusCode(Ok).and(jsonBody[ScenarioAttachments].example(Examples.scenarioAttachments))) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + } + + val addAttachmentEndpoint: SecuredEndpoint[AddAttachmentRequest, ScenarioActivityError, Unit, Any] = { + baseNuApiEndpoint + .summary("Add scenario attachment service") + .tag("Activities") + .post + .in("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" / "attachments") + .in(streamProvider.streamBodyEndpointInput) + .in(header[FileName](HeaderNames.ContentDisposition)) + .mapInTo[AddAttachmentRequest] + .out(statusCode(Ok)) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + } + + val downloadAttachmentEndpoint + : SecuredEndpoint[GetAttachmentRequest, ScenarioActivityError, GetAttachmentResponse, Any] = { + baseNuApiEndpoint + .summary("Download attachment service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity" / "attachments" / path[Long]("attachmentId")) + .mapInTo[GetAttachmentRequest] + .out( + statusCode(Ok) + .and(streamProvider.streamBodyEndpointOutput) + .and(header(HeaderNames.ContentDisposition)(optionalHeaderCodec)) + .and(header(HeaderNames.ContentType)(requiredHeaderCodec)) + .mapTo[GetAttachmentResponse] + ) + .errorOut(scenarioNotFoundErrorOutput) + .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 new file mode 100644 index 00000000000..2812469e4eb --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Examples.scala @@ -0,0 +1,217 @@ +package pl.touk.nussknacker.ui.api.description.scenarioActivity + +import pl.touk.nussknacker.engine.api.process.ProcessName +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError.{NoComment, NoScenario} +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos._ +import sttp.tapir.EndpointIO.Example + +import java.time.Instant +import java.util.UUID + +object Examples { + + val scenarioActivities: Example[ScenarioActivities] = Example.of( + summary = Some("Display scenario actions"), + value = ScenarioActivities( + activities = List( + ScenarioActivity.ScenarioCreated( + id = UUID.fromString("80c95497-3b53-4435-b2d9-ae73c5766213"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + ), + ScenarioActivity.ScenarioArchived( + id = UUID.fromString("070a4e5c-21e5-4e63-acac-0052cf705a90"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + ), + ScenarioActivity.ScenarioUnarchived( + id = UUID.fromString("fa35d944-fe20-4c4f-96c6-316b6197951a"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + ), + ScenarioActivity.ScenarioDeployed( + id = UUID.fromString("545b7d87-8cdf-4cb5-92c4-38ddbfca3d08"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Deployment of scenario - task JIRA-1234"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + ScenarioActivity.ScenarioCanceled( + id = UUID.fromString("c354eba1-de97-455c-b977-74729c41ce7"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Canceled because marketing campaign ended"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + ScenarioActivity.ScenarioModified( + id = UUID.fromString("07b04d45-c7c0-4980-a3bc-3c7f66410f68"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Added new processing step"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + ScenarioActivity.ScenarioNameChanged( + id = UUID.fromString("da3d1f78-7d73-4ed9-b0e5-95538e150d0d"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Changed name to better replect the purpose of this scenario"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ), + oldName = "marketing campaign", + newName = "old marketing campaign", + ), + ScenarioActivity.CommentAdded( + id = UUID.fromString("edf8b047-9165-445d-a173-ba61812dbd63"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Added new processing step"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + ScenarioActivity.CommentAdded( + id = UUID.fromString("369367d6-d445-4327-ac23-4a94367b1d9e"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = None, + lastModifiedBy = "John Doe", + lastModifiedAt = Instant.parse("2024-01-18T14:21:17Z") + ) + ), + ScenarioActivity.AttachmentAdded( + id = UUID.fromString("b29916a9-34d4-4fc2-a6ab-79569f68c0b2"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + attachment = ScenarioActivityAttachment( + id = Some(10000001), + filename = "attachment01.png", + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ), + ), + ScenarioActivity.AttachmentAdded( + id = UUID.fromString("d0a7f4a2-abcc-4ffa-b1ca-68f6da3e999a"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + attachment = ScenarioActivityAttachment( + id = None, + filename = "attachment01.png", + lastModifiedBy = "John Doe", + lastModifiedAt = Instant.parse("2024-01-18T14:21:17Z") + ), + ), + ScenarioActivity.ChangedProcessingMode( + id = UUID.fromString("683df470-0b33-4ead-bf61-fa35c63484f3"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + from = "Request-Response", + to = "Batch", + ), + ScenarioActivity.IncomingMigration( + id = UUID.fromString("4da0f1ac-034a-49b6-81c9-8ee48ba1d830"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + sourceEnvironment = "preprod", + sourceScenarioVersion = "23", + ), + ScenarioActivity.OutgoingMigration( + id = UUID.fromString("49fcd45d-3fa6-48d4-b8ed-b3055910c7ad"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Added new processing step"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ), + destinationEnvironment = "preprod", + ), + ScenarioActivity.PerformedSingleExecution( + id = UUID.fromString("924dfcd3-fbc7-44ea-8763-813874382204"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + errorMessage = Some("Execution error occurred"), + ), + ScenarioActivity.PerformedSingleExecution( + id = UUID.fromString("924dfcd3-fbc7-44ea-8763-813874382204"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + errorMessage = None, + ), + ScenarioActivity.PerformedScheduledExecution( + id = UUID.fromString("9b27797e-aa03-42ba-8406-d0ae8005a883"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + errorMessage = None, + ), + ScenarioActivity.AutomaticUpdate( + id = UUID.fromString("33509d37-7657-4229-940f-b5736c82fb13"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + changes = "JIRA-12345, JIRA-32146", + errorMessage = None, + ), + ), + ) + ) + + val scenarioAttachments: Example[ScenarioAttachments] = Example.of( + summary = Some("Display scenario activity"), + value = ScenarioAttachments( + attachments = List( + Attachment( + id = 1L, + scenarioVersion = 1L, + fileName = "some_file.txt", + user = "test", + createDate = Instant.parse("2024-01-17T14:21:17Z") + ) + ) + ) + ) + + val noScenarioError: Example[NoScenario] = Example.of( + summary = Some("No scenario {scenarioName} found"), + value = NoScenario(ProcessName("'example scenario'")) + ) + + val commentNotFoundError: Example[NoComment] = Example.of( + summary = Some("Unable to edit comment with id: {commentId}"), + value = NoComment("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 new file mode 100644 index 00000000000..8c7a031a7d7 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/InputOutput.scala @@ -0,0 +1,26 @@ +package pl.touk.nussknacker.ui.api.description.scenarioActivity + +import pl.touk.nussknacker.engine.api.process.ProcessName +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError.NoScenario +import sttp.model.StatusCode.NotFound +import sttp.tapir.EndpointIO.Example +import sttp.tapir.{EndpointOutput, oneOf, oneOfVariantFromMatchType, plainBody} + +object InputOutput { + + val scenarioNotFoundErrorOutput: EndpointOutput.OneOf[ScenarioActivityError, ScenarioActivityError] = + oneOf[ScenarioActivityError]( + oneOfVariantFromMatchType( + NotFound, + plainBody[NoScenario] + .example( + Example.of( + summary = Some("No scenario {scenarioName} found"), + value = NoScenario(ProcessName("'example scenario'")) + ) + ) + ) + ) + +} 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 eec3241313c..35e28aebfd2 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 TagsEntityFactory 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..98b8b4306eb --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/ScenarioActivityEntityFactory.scala @@ -0,0 +1,165 @@ +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 + +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) + + def userId: Rep[String] = column[String]("user_id", NotNull) + + 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 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, + createdAt, + scenarioVersion, + comment, + attachmentId, + performedAt, + state, + errorMessage, + buildInfo, + additionalProperties, + ) <> ( + ScenarioActivityEntityData.apply _ tupled, ScenarioActivityEntityData.unapply + ) + + } + + implicit def scenarioActivityTypeMapper: BaseColumnType[ScenarioActivityType] = + MappedColumnType.base[ScenarioActivityType, String](_.entryName, ScenarioActivityType.withName) + + 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 + + override def values: immutable.IndexedSeq[ScenarioActivityType] = findValues + +} + +final case class AdditionalProperties(properties: Map[String, String]) + +object AdditionalProperties { + def empty: AdditionalProperties = AdditionalProperties(Map.empty) +} + +final case class ScenarioActivityEntityData( + id: Long, + activityType: ScenarioActivityType, // actionName: ScenarioActionName + scenarioId: ProcessId, // processId: ProcessId, + activityId: ScenarioActivityId, // id: ProcessActionId + userId: String, // user: String, + userName: String, // user: String, + impersonatedByUserId: Option[String], // impersonatedByIdentity: Option[String] + impersonatedByUserName: Option[String], // impersonatedByUsername: Option[String] + lastModifiedByUserName: Option[String], // user: String, + createdAt: Timestamp, // createdAt: Timestamp, + scenarioVersion: Option[ScenarioVersion], // processVersionId: Option[VersionId], + + comment: Option[String], // commentId: Option[Long], + attachmentId: Option[Long], + finishedAt: Option[Timestamp], // performedAt: Option[Timestamp], + state: Option[ProcessActionState], // state: ProcessActionState, + errorMessage: Option[String], // failureMessage: Option[String], + buildInfo: 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 6483a67b138..de5381d8de8 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,6 +12,7 @@ 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 @@ -28,10 +28,10 @@ object Initialization { migrations: ProcessingTypeDataProvider[ProcessMigrations, _], db: DbRef, fetchingRepository: DBFetchingProcessRepository[DB], - commentRepository: CommentRepository, - environment: String + scenarioActivityRepository: ScenarioActivityRepository, + environment: String, )(implicit ec: ExecutionContext): Unit = { - val processRepository = new DBProcessRepository(db, commentRepository, migrations.mapValues(_.version)) + val processRepository = new DBProcessRepository(db, scenarioActivityRepository, 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..e0d6c2078e0 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, ProcessActionRepository} 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, + processActionRepository: ProcessActionRepository, dbioRunner: DBIOActionRunner, config: NotificationConfig, clock: Clock = Clock.systemUTC() @@ -30,60 +28,28 @@ class NotificationServiceImpl( val limit = now.minusMillis(config.duration.toMillis) dbioRunner .run( - actionRepository.getUserActionsAfter( + processActionRepository.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, processName) => + action.state match { + case ProcessActionState.Finished => + Notification + .actionFinishedNotification(action.id.toString, action.actionName, processName) + case ProcessActionState.Failed => + Notification + .actionFailedNotification(action.id.toString, action.actionName, processName, action.failureMessage) + case ProcessActionState.ExecutionFinished => + Notification + .actionExecutionFinishedNotification(action.id.toString, action.actionName, processName) + case ProcessActionState.InProgress => + throw new IllegalStateException(s"Unexpected action returned by query: $action, for scenario: $processName") + } + }) } } 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 95d0af8567e..61f37ff8fc1 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 DbProcessActionRepository(dbRef, dumbModelInfoProvier) val processRepository = DBFetchingProcessRepository.create(dbRef, actionRepository) val futureProcessRepository = DBFetchingProcessRepository.createFutureRepository(dbRef, actionRepository) new DefaultProcessingTypeDeployedScenariosProvider( 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..ddd4195905e 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 @@ -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..52840daa9d9 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,15 +1,14 @@ 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.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 scala.concurrent.{ExecutionContext, Future} @@ -18,7 +17,7 @@ import scala.concurrent.{ExecutionContext, Future} // 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 )(implicit ec: ExecutionContext) { @@ -41,15 +40,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) = @@ -61,14 +59,18 @@ class ActivityService( scenarioId: ProcessId, scenarioGraphVersionId: VersionId, user: LoggedUser - ) = + ): EitherT[Future, ActivityError[ErrorType], Unit] = { + EitherT.right[ActivityError[ErrorType]]( - commentOpt - .map(comment => - dbioRunner.run(commentRepository.saveComment(scenarioId, scenarioGraphVersionId, user, comment)) - ) - .sequence + Future.unit +// todo NU-1772 in progress +// commentOpt +// .map(comment => +// dbioRunner.run(scenarioActivityRepository.addComment(scenarioId, scenarioGraphVersionId, comment)(user)) +// ) +// .getOrElse(Future.unit) ) + } } 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/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 index b6012f57d26..4de36a5fadb 100644 --- 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 @@ -1,23 +1,18 @@ package pl.touk.nussknacker.ui.process.repository -import cats.implicits.toTraverseOps +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.{ - ProcessAction, - ProcessActionId, - ProcessActionState, - ScenarioActionName -} +import pl.touk.nussknacker.engine.api.deployment._ 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.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.{ImpersonatedUser, LoggedUser, RealLoggedUser} +import pl.touk.nussknacker.ui.security.api.LoggedUser import slick.dbio.DBIOAction import java.sql.Timestamp @@ -27,12 +22,24 @@ 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 + 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( @@ -40,11 +47,17 @@ trait ProcessActionRepository { 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 DbProcessActionRepository( protected val dbRef: DbRef, - commentRepository: CommentRepository, buildInfos: ProcessingTypeDataProvider[Map[String, String], _] )(implicit ec: ExecutionContext) extends DbioRepository @@ -71,9 +84,9 @@ class DbProcessActionRepository( createdAt = now, performedAt = None, failure = None, - commentId = None, + comment = None, buildInfoProcessingType = buildInfoProcessingType - ).map(_.id) + ).map(_.activityId.value).map(ProcessActionId.apply) ) } @@ -89,8 +102,7 @@ class DbProcessActionRepository( 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)) + updated <- updateAction(actionId, ProcessActionState.Finished, Some(performedAt), None, comment) _ <- if (updated) { DBIOAction.successful(()) @@ -105,7 +117,7 @@ class DbProcessActionRepository( performedAt, Some(performedAt), None, - comment.map(_.id), + comment, buildInfoProcessingType ) } @@ -148,16 +160,16 @@ class DbProcessActionRepository( def markFinishedActionAsExecutionFinished(actionId: ProcessActionId): DB[Boolean] = { run( - processActionsTable - .filter(a => a.id === actionId && a.state === ProcessActionState.Finished) + scenarioActivityTable + .filter(a => a.activityId === activityId(actionId) && a.state === ProcessActionState.Finished) .map(_.state) - .update(ProcessActionState.ExecutionFinished) + .update(Some(ProcessActionState.ExecutionFinished)) .map(_ == 1) ) } def removeAction(actionId: ProcessActionId): DB[Unit] = { - run(processActionsTable.filter(a => a.id === actionId).delete.map(_ => ())) + run(scenarioActivityTable.filter(a => a.activityId === activityId(actionId)).delete.map(_ => ())) } override def markProcessAsArchived(processId: ProcessId, processVersion: VersionId)( @@ -178,9 +190,8 @@ class DbProcessActionRepository( buildInfoProcessingType: Option[ProcessingType] )(implicit user: LoggedUser): DB[ProcessAction] = { val now = Instant.now() - run(for { - comment <- saveCommentWhenPassed(processId, processVersion, comment) - result <- insertAction( + run( + insertAction( None, processId, Some(processVersion), @@ -189,10 +200,10 @@ class DbProcessActionRepository( now, Some(now), None, - comment.map(_.id), + comment, buildInfoProcessingType - ) - } yield toFinishedProcessAction(result, comment)) + ).map(a => toFinishedProcessAction(a)) + ) } private def insertAction( @@ -204,30 +215,52 @@ class DbProcessActionRepository( createdAt: Instant, performedAt: Option[Instant], failure: Option[String], - commentId: Option[Long], + comment: Option[Comment], buildInfoProcessingType: Option[ProcessingType] - )(implicit user: LoggedUser): DB[ProcessActionEntityData] = { + )(implicit user: LoggedUser): DB[ScenarioActivityEntityData] = { 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, + + 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 other => + throw new IllegalArgumentException(s"Action with id: $other can't be inserted") + } + val entity = ScenarioActivityEntityData( + id = -1, + activityType = activityType, + scenarioId = processId, + activityId = ScenarioActivityId(actionIdOpt.map(_.value).getOrElse(UUID.randomUUID())), + userId = user.id, + userName = user.username, + impersonatedByUserId = user.impersonatingUserId, + impersonatedByUserName = user.impersonatingUserName, + lastModifiedByUserName = None, createdAt = Timestamp.from(createdAt), - performedAt = performedAt.map(Timestamp.from), - actionName = actionName, - state = state, - failureMessage = failure, - commentId = commentId, - buildInfo = buildInfoJsonOpt + 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) ) - (processActionsTable += processActionData).map { insertCount => + (scenarioActivityTable += entity).map { insertCount => if (insertCount != 1) throw new IllegalArgumentException(s"Action with id: $actionId can't be inserted") - processActionData + entity } } @@ -236,156 +269,223 @@ class DbProcessActionRepository( state: ProcessActionState, performedAt: Option[Instant], failure: Option[String], - commentId: Option[Long] + comment: Option[Comment], ): 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)) + 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(processActionsTable.filter(_ => false).forUpdate.result.map(_ => ())) + run(scenarioActivityTable.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) + val query = scenarioActivityTable + .filter(action => action.scenarioId === processId && action.state === ProcessActionState.InProgress) + .map(_.activityType) .distinct - run(query.result.map(_.toSet)) + run(query.result.map(_.toSet.flatMap(actionName))) } def getInProgressActionNames( allowedActionNames: Set[ScenarioActionName] ): DB[Map[ProcessId, Set[ScenarioActionName]]] = { - val query = processActionsTable + val query = scenarioActivityTable .filter(action => action.state === ProcessActionState.InProgress && - action.actionName - .inSet(allowedActionNames) + action.activityType + .inSet(activityTypes(allowedActionNames)) ) - .map(pa => (pa.processId, pa.actionName)) + .map(pa => (pa.scenarioId, pa.activityType)) run( query.result - .map(_.groupBy { case (process_id, _) => process_id } - .mapValuesNow(_.map(_._2).toSet)) + .map(_.groupBy { case (process_id, _) => ProcessId(process_id.value) } + .mapValuesNow(_.map(_._2).toSet.flatMap(actionName))) ) } def getUserActionsAfter( - user: LoggedUser, + loggedUser: LoggedUser, possibleActionNames: Set[ScenarioActionName], possibleStates: Set[ProcessActionState], limit: Instant - ): DB[Seq[(ProcessActionEntityData, ProcessName)]] = { + ): DB[List[(ProcessAction, ProcessName)]] = { run( - processActionsTable + scenarioActivityTable .filter(a => - a.user === user.username && a.state.inSet(possibleStates) && a.actionName.inSet( - possibleActionNames + 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.processId) + .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(processActionsTable.filter(_.state === ProcessActionState.InProgress).delete.map(_ => ())) + run(scenarioActivityTable.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 activityTypes = actionNamesOpt.getOrElse(Set.empty).flatMap(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(_.processId) + .groupBy(_.scenarioId) .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 + .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 ((processId, _), action) => processId -> action } - .joinLeft(commentsTable) - .on { case ((_, action), comment) => action.commentId === comment.id } - .map { case ((processId, action), comment) => processId -> (action, comment) } + .map { case ((scenarioId, _), activity) => scenarioId -> activity } run( - finalQuery.result.map(_.toMap.mapValuesNow(toFinishedProcessAction)) + finalQuery.result.map(_.map { case (scenarioId, action) => + (ProcessId(scenarioId.value), toFinishedProcessAction(action)) + }.toMap) ) } override def getFinishedProcessAction( actionId: ProcessActionId - )(implicit ec: ExecutionContext): DB[Option[ProcessAction]] = + ): 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 } + 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]])( - implicit ec: ExecutionContext + override def getFinishedProcessActions( + processId: ProcessId, + actionNamesOpt: Option[Set[ScenarioActionName]] ): 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) + val query = scenarioActivityTable + .filter(p => p.scenarioId === processId && p.state.inSet(ProcessActionState.FinishedStates)) + .sortBy(_.performedAt.desc) run( actionNamesOpt - .map(actionNames => query.filter { case (entity, _) => entity.actionName.inSet(actionNames) }) + .map(actionNames => query.filter { entity => entity.activityType.inSet(activityTypes(actionNames)) }) .getOrElse(query) .result .map(_.toList.map(toFinishedProcessAction)) ) } - private def toFinishedProcessAction(actionData: (ProcessActionEntityData, Option[CommentEntityData])): ProcessAction = + private def toFinishedProcessAction(activityEntity: ScenarioActivityEntityData): 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) + 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 = None, // todo NU-1772 in progress - pass the entire comment? + comment = activityEntity.comment.map(_.value), + buildInfo = activityEntity.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 + 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 + } + } + + private def activityTypes(actionNames: Set[ScenarioActionName]): Set[ScenarioActivityType] = { + actionNames.flatMap(activityType) + } + + private def activityType(actionName: ScenarioActionName): Option[ScenarioActivityType] = { + actionName match { + case ScenarioActionName.Deploy => + Some(ScenarioActivityType.ScenarioDeployed) + case ScenarioActionName.Cancel => + Some(ScenarioActivityType.ScenarioCanceled) + case ScenarioActionName.Archive => + Some(ScenarioActivityType.ScenarioArchived) + case ScenarioActionName.UnArchive => + Some(ScenarioActivityType.ScenarioUnarchived) + case ScenarioActionName.Pause => + Some(ScenarioActivityType.ScenarioPaused) + case ScenarioActionName.Rename => + Some(ScenarioActivityType.ScenarioNameChanged) + case _ => + None + } + } } 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 78f2bbe1ef4..280acb47b0e 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 @@ -19,11 +20,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 @@ -43,10 +45,10 @@ object ProcessRepository { def create( dbRef: DbRef, - commentRepository: CommentRepository, - migrations: ProcessingTypeDataProvider[ProcessMigrations, _] + scenarioActivityRepository: ScenarioActivityRepository, + migrations: ProcessingTypeDataProvider[ProcessMigrations, _], ): DBProcessRepository = - new DBProcessRepository(dbRef, commentRepository, migrations.mapValues(_.version)) + new DBProcessRepository(dbRef, scenarioActivityRepository, migrations.mapValues(_.version)) final case class CreateProcessAction( processName: ProcessName, @@ -88,8 +90,8 @@ trait ProcessRepository[F[_]] { class DBProcessRepository( protected val dbRef: DbRef, - commentRepository: CommentRepository, - modelVersion: ProcessingTypeDataProvider[Int, _] + scenarioActivityRepository: ScenarioActivityRepository, + modelVersion: ProcessingTypeDataProvider[Int, _], ) extends ProcessRepository[DB] with NuTables with LazyLogging @@ -153,9 +155,28 @@ 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 = User( + id = 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), + ) + ) + ) + ) + }.sequence } updateProcessInternal( @@ -268,11 +289,23 @@ 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 = User( + id = 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)), + // todo NU-1772 in progress + comment = ScenarioComment.Available("todomgw", UserName(loggedUser.username)), + 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/activities/DbScenarioActivityRepository.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/activities/DbScenarioActivityRepository.scala new file mode 100644 index 00000000000..ea2ec097839 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/activities/DbScenarioActivityRepository.scala @@ -0,0 +1,647 @@ +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.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 java.sql.Timestamp +import java.time.Instant +import scala.concurrent.ExecutionContext + +class DbScenarioActivityRepository(override protected val dbRef: DbRef)( + implicit executionContext: ExecutionContext, +) extends DbioRepository + with NuTables + with ScenarioActivityRepository + with LazyLogging { + + import dbRef.profile.api._ + + def fetchActivities( + scenarioId: ProcessId, + )(implicit user: LoggedUser): DB[Seq[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) + } + } + } + + 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] = { + insertActivity( + ScenarioActivity.CommentAdded( + scenarioId = ScenarioId(scenarioId.value), + scenarioActivityId = ScenarioActivityId.random, + user = toUser(user), + date = Instant.now(), + scenarioVersion = Some(ScenarioVersion(processVersionId.value)), + comment = ScenarioComment.Available( + comment = comment, + lastModifiedByUserName = UserName(user.username), + ) + ), + ).map(_.activityId) + } + + def editComment( + activityId: ScenarioActivityId, + comment: String + )(implicit user: LoggedUser): DB[Either[ModifyCommentError, Unit]] = { + modifyActivity( + activityId = activityId, + activityDoesNotExistError = ModifyCommentError.ActivityDoesNotExist, + validateCurrentValue = _.comment.toRight(ModifyCommentError.CommentDoesNotExist).map(_ => ()), + modify = _.copy(comment = Some(comment), lastModifiedByUserName = Some(user.username)), + couldNotModifyError = ModifyCommentError.CouldNotModifyComment, + ) + } + + def deleteComment( + activityId: ScenarioActivityId, + )(implicit user: LoggedUser): DB[Either[ModifyCommentError, Unit]] = { + modifyActivity( + activityId = activityId, + activityDoesNotExistError = ModifyCommentError.ActivityDoesNotExist, + validateCurrentValue = _.comment.toRight(ModifyCommentError.CommentDoesNotExist).map(_ => ()), + modify = _.copy(comment = None, lastModifiedByUserName = Some(user.username)), + couldNotModifyError = ModifyCommentError.CouldNotModifyComment, + ) + } + + def addAttachment( + attachmentToAdd: AttachmentToAdd + )(implicit user: LoggedUser): DB[ScenarioActivityId] = { + 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(Instant.now()) + ) + activity <- insertActivity( + ScenarioActivity.AttachmentAdded( + scenarioId = ScenarioId(attachmentToAdd.scenarioId.value), + scenarioActivityId = ScenarioActivityId.random, + user = toUser(user), + date = Instant.now(), + scenarioVersion = Some(ScenarioVersion(attachmentToAdd.scenarioVersionId.value)), + attachment = ScenarioAttachment.Available( + attachmentId = AttachmentId(attachment.id), + attachmentFilename = AttachmentFilename(attachmentToAdd.fileName), + lastModifiedByUserName = UserName(user.username), + ) + ), + ) + } yield activity.activityId + } + + def findAttachment( + scenarioId: ProcessId, + attachmentId: Long, + ): DB[Option[AttachmentEntityData]] = { + attachmentsTable + .filter(_.id === attachmentId) + .filter(_.processId === scenarioId) + .result + .headOption + } + + def findActivity( + processId: ProcessId + ): DB[String] = ??? + + def getActivityStats: DB[Map[String, Int]] = ??? + + private def toUser(loggedUser: LoggedUser) = { + User( + id = UserId(loggedUser.id), + name = UserName(loggedUser.username), + impersonatedByUserId = loggedUser.impersonatingUserId.map(UserId.apply), + impersonatedByUserName = loggedUser.impersonatingUserName.map(UserName.apply) + ) + } + + 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 modifyActivity[ERROR]( + activityId: ScenarioActivityId, + activityDoesNotExistError: ERROR, + validateCurrentValue: ScenarioActivityEntityData => Either[ERROR, Unit], + modify: ScenarioActivityEntityData => ScenarioActivityEntityData, + couldNotModifyError: ERROR, + ): DB[Either[ERROR, Unit]] = { + val action = for { + dataPulled <- activityByIdCompiled(activityId).result.headOption + result <- { + val modifiedEntity = for { + entity <- dataPulled.toRight(activityDoesNotExistError) + _ <- validateCurrentValue(entity) + modifiedEntity = modify(entity) + } yield modifiedEntity + + modifiedEntity match { + case Left(error) => + DBIO.successful(Left(error)) + case Right(modifiedEntity) => + for { + rowsAffected <- activityByIdCompiled(activityId).update(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 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 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 + } + ScenarioActivityEntityData( + id = -1, + activityType = activityType, + scenarioId = ProcessId(scenarioActivity.scenarioId.value), + activityId = ScenarioActivityId.random, + userId = scenarioActivity.user.id.value, + userName = scenarioActivity.user.name.value, + impersonatedByUserId = scenarioActivity.user.impersonatedByUserId.map(_.value), + impersonatedByUserName = scenarioActivity.user.impersonatedByUserName.map(_.value), + lastModifiedByUserName = lastModifiedByUserName, + createdAt = Timestamp.from(Instant.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) + } + + 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)( + comment = comment(activity.comment), + lastModifiedByUserName = lastModifiedByUserName(activity.comment), + 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 = Some(Timestamp.from(activity.dateFinished)), + errorMessage = activity.errorMessage, + ) + case activity: ScenarioActivity.PerformedScheduledExecution => + createEntity(scenarioActivity)( + finishedAt = Some(Timestamp.from(activity.dateFinished)), + errorMessage = activity.errorMessage, + // todomgw execution params + ) + case activity: ScenarioActivity.AutomaticUpdate => + createEntity(scenarioActivity)( + finishedAt = Some(Timestamp.from(activity.dateFinished)), + errorMessage = activity.errorMessage, + additionalProperties = AdditionalProperties( + Map( + "description" -> activity.changes.mkString(",\n"), + ) + ) + ) + } + } + + private def userFromEntity(entity: ScenarioActivityEntityData): User = { + User( + id = UserId(entity.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") + } yield { + entity.comment match { + case Some(comment) => + ScenarioComment.Available(comment = comment, lastModifiedByUserName = UserName(lastModifiedByUserName)) + case None => + ScenarioComment.Deleted(deletedByUserName = UserName(lastModifiedByUserName)) + } + } + } + + private def attachmentFromEntity(entity: ScenarioActivityEntityData): Either[String, ScenarioAttachment] = { + for { + lastModifiedByUserName <- entity.lastModifiedByUserName.toRight("Missing lastModifiedByUserName field") + filename <- additionalPropertyFromEntity(entity, "attachmentFilename") + } yield { + entity.attachmentId match { + case Some(id) => + ScenarioAttachment.Available( + attachmentId = AttachmentId(id), + attachmentFilename = AttachmentFilename(filename), + lastModifiedByUserName = UserName(lastModifiedByUserName) + ) + case None => + ScenarioAttachment.Deleted( + attachmentFilename = AttachmentFilename(filename), + deletedByUserName = UserName(lastModifiedByUserName) + ) + } + } + } + + private def additionalPropertyFromEntity(entity: ScenarioActivityEntityData, name: String): Either[String, String] = { + entity.additionalProperties.properties.get(name).toRight(s"Missing additional property $name") + } + + def fromEntity(entity: ScenarioActivityEntityData): Either[String, 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 + case ScenarioActivityType.ScenarioArchived => + ScenarioActivity + .ScenarioArchived( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion + ) + .asRight + case ScenarioActivityType.ScenarioUnarchived => + ScenarioActivity + .ScenarioUnarchived( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion + ) + .asRight + 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, + ) + } + 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, + ) + } + 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, + ) + } + 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, + ) + } + case ScenarioActivityType.ScenarioNameChanged => + for { + comment <- commentFromEntity(entity) + oldName <- additionalPropertyFromEntity(entity, "oldName") + newName <- additionalPropertyFromEntity(entity, "newName") + } yield ScenarioActivity.ScenarioNameChanged( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + comment = comment, + oldName = oldName, + newName = newName + ) + 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, + ) + 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, + ) + 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, + ) + 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) + ) + 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), + ) + case ScenarioActivityType.PerformedSingleExecution => + for { + finishedAt <- entity.finishedAt.map(_.toInstant).toRight("Missing finishedAt") + } yield ScenarioActivity.PerformedSingleExecution( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + dateFinished = finishedAt, + errorMessage = entity.errorMessage, + ) + case ScenarioActivityType.PerformedScheduledExecution => + for { + finishedAt <- entity.finishedAt.map(_.toInstant).toRight("Missing finishedAt") + } yield ScenarioActivity.PerformedScheduledExecution( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + dateFinished = finishedAt, + errorMessage = entity.errorMessage, + ) + 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, + ) + } + } + +} 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..43ee59dfbb1 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/activities/ScenarioActivityRepository.scala @@ -0,0 +1,63 @@ +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.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 fetchActivities( + scenarioId: ProcessId, + )(implicit user: LoggedUser): 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( + scenarioActivityId: ScenarioActivityId, + comment: String, + )(implicit user: LoggedUser): DB[Either[ModifyCommentError, Unit]] + + def deleteComment( + scenarioActivityId: ScenarioActivityId + )(implicit user: LoggedUser): DB[Either[ModifyCommentError, Unit]] + + def addAttachment( + attachmentToAdd: AttachmentToAdd + )(implicit user: LoggedUser): DB[ScenarioActivityId] + + def findAttachment( + scenarioId: ProcessId, + attachmentId: Long, + ): DB[Option[AttachmentEntityData]] + + def findActivity( + processId: ProcessId + ): DB[String] + + 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 cf335682763..332ed201804 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 @@ -65,6 +65,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 @@ -151,12 +152,12 @@ 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 processRepository = DBFetchingProcessRepository.create(dbRef, actionRepository) + val scenarioActivityRepository = new DbScenarioActivityRepository(dbRef) + val actionRepository = new DbProcessActionRepository(dbRef, modelBuildInfo) + val processRepository = DBFetchingProcessRepository.create(dbRef, actionRepository) // 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) - val writeProcessRepository = ProcessRepository.create(dbRef, commentRepository, migrations) + val writeProcessRepository = ProcessRepository.create(dbRef, scenarioActivityRepository, migrations) val fragmentRepository = new DefaultFragmentRepository(futureProcessRepository) val fragmentResolver = new FragmentResolver(fragmentRepository) @@ -233,12 +234,12 @@ 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) val authenticationResources = AuthenticationResources(resolvedConfig, getClass.getClassLoader, sttpBackend) val authManager = new AuthManager(authenticationResources) - Initialization.init(migrations, dbRef, processRepository, commentRepository, environment) + Initialization.init(migrations, dbRef, processRepository, processActivityRepository, environment) val newProcessPreparer = processingTypeDataProvider.mapValues { processingTypeData => new NewProcessPreparer( @@ -352,14 +353,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, @@ -394,11 +397,10 @@ class AkkaHttpBasedRouteProvider( dbioRunner, Clock.systemDefaultZone() ) - val commentRepository = new CommentRepository(dbRef) val activityService = new ActivityService( featureTogglesConfig.deploymentCommentSettings, - commentRepository, + scenarioActivityRepository, deploymentService, dbioRunner ) @@ -416,12 +418,13 @@ class AkkaHttpBasedRouteProvider( processAuthorizer = processAuthorizer, processChangeListener = processChangeListener ), - new ProcessesExportResources( - futureProcessRepository, - processService, - processActivityRepository, - processResolver - ), +// todo NU-1772 in progress +// new ProcessesExportResources( +// futureProcessRepository, +// processService, +// scenarioActivityRepository, +// processResolver +// ), new ManagementResources( processAuthorizer, processService, @@ -494,7 +497,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..abf6b26e386 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 @@ -1,391 +1,392 @@ -package pl.touk.nussknacker.ui.util - -import java.io._ -import java.net.URI -import java.nio.charset.StandardCharsets -import java.time.{Instant, ZoneId} -import java.time.format.DateTimeFormatter -import javax.xml.transform.TransformerFactory -import javax.xml.transform.sax.SAXResult -import javax.xml.transform.stream.StreamSource -import com.typesafe.scalalogging.LazyLogging -import org.apache.commons.io.IOUtils -import org.apache.fop.apps.FopConfParser -import org.apache.fop.apps.io.ResourceResolverFactory -import org.apache.xmlgraphics.util.MimeConstants -import pl.touk.nussknacker.engine.api.graph.ScenarioGraph -import pl.touk.nussknacker.engine.graph.node._ -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.process.repository.ScenarioWithDetailsEntity - -import scala.xml.{Elem, NodeSeq, XML} - -object PdfExporter extends LazyLogging { - - private val fopFactory = new FopConfParser( - getClass.getResourceAsStream("/fop/config.xml"), - new URI("http://touk.pl"), - ResourceResolverFactory.createDefaultResourceResolver - ).getFopFactoryBuilder.build - - def exportToPdf( - svg: String, - processDetails: ScenarioWithDetailsEntity[ScenarioGraph], - processActivity: ProcessActivity - ): Array[Byte] = { - - // initFontsIfNeeded is invoked every time to make sure that /tmp content is not deleted - initFontsIfNeeded() - // FIXME: cannot render polish signs..., better to strip them than not render anything... - // \u00A0 - non-breaking space in not ASCII :)... - val fopXml = prepareFopXml( - svg.replaceAll("\u00A0", " ").replaceAll("[^\\p{ASCII}]", ""), - processDetails, - processActivity, - processDetails.json - ) - - createPdf(fopXml) - } - - // in PDF export we print timezone, to avoid ambiguity - // TODO: pass client timezone from FE - private def format(instant: Instant) = { - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss [VV]") - instant.atZone(ZoneId.systemDefault()).format(formatter) - } - - // TODO: this is one nasty hack, is there a better way to make fop read fonts from classpath? - private def initFontsIfNeeded(): Unit = synchronized { - val dir = new File("/tmp/fop/fonts") - dir.mkdirs() - List( - "OpenSans-BoldItalic.ttf", - "OpenSans-Bold.ttf", - "OpenSans-ExtraBoldItalic.ttf", - "OpenSans-ExtraBold.ttf", - "OpenSans-Italic.ttf", - "OpenSans-LightItalic.ttf", - "OpenSans-Light.ttf", - "OpenSans-Regular.ttf", - "OpenSans-SemiboldItalic.ttf", - "OpenSans-Semibold.ttf" - ).filterNot(name => new File(dir, name).exists()).foreach { name => - IOUtils.copy(getClass.getResourceAsStream(s"/fop/fonts/$name"), new FileOutputStream(new File(dir, name))) - } - } - - private def createPdf(fopXml: Elem): Array[Byte] = { - val out = new ByteArrayOutputStream() - val fop = fopFactory.newFop(MimeConstants.MIME_PDF, out) - val src = new StreamSource(new ByteArrayInputStream(fopXml.toString().getBytes(StandardCharsets.UTF_8))) - TransformerFactory.newInstance().newTransformer().transform(src, new SAXResult(fop.getDefaultHandler)) - out.toByteArray - } - - private def prepareFopXml( - svg: String, - processDetails: ScenarioWithDetailsEntity[ScenarioGraph], - processActivity: ProcessActivity, - scenarioGraph: ScenarioGraph - ) = { - val diagram = XML.loadString(svg) - val currentVersion = processDetails.history.get.find(_.processVersionId == processDetails.processVersionId).get - - - - - - - - - - - - - - - - - - - - - - {processDetails.name} - ( - {processDetails.processCategory} - ) - - - - Version: - {processDetails.processVersionId} - - - Saved by - {currentVersion.user} - at - {format(currentVersion.createDate)} - - - {processDetails.description.getOrElse("")} - - - - {diagram} - - - {nodesSummary(scenarioGraph)} - Nodes details - {scenarioGraph.nodes.map(nodeDetails)}{comments(processActivity)}{attachments(processActivity)} - - - - - } - - private def comments(processActivity: ProcessActivity) = - - - Comments - - - - - - - - - Date - - - Author - - - Comment - - - - - { - if (processActivity.comments.isEmpty) { - - - - } else - processActivity.comments.sortBy(_.createDate).map { comment => - - - - {format(comment.createDate)} - - - - - {comment.user} - - - - - {comment.content} - - - - } - } - -
-
-
- - private def nodeDetails(node: NodeData) = { - val nodeData = node match { - case Source(_, SourceRef(typ, params), _) => ("Type", typ) :: params.map(p => (p.name, p.expression.expression)) - case Filter(_, expression, _, _) => List(("Expression", expression.expression)) - case Enricher(_, ServiceRef(typ, params), output, _) => - ("Type", typ) :: ("Output", output) :: params.map(p => (p.name, p.expression.expression)) - // TODO: what about Swtich?? - case Switch(_, expression, exprVal, _) => expression.map(e => ("Expression", e.expression)).toList - case Processor(_, ServiceRef(typ, params), _, _) => - ("Type", typ) :: params.map(p => (p.name, p.expression.expression)) - case Sink(_, SinkRef(typ, params), _, _, _) => ("Type", typ) :: params.map(p => (p.name, p.expression.expression)) - case CustomNode(_, output, typ, params, _) => - ("Type", typ) :: ("Output", output.getOrElse("")) :: params.map(p => (p.name, p.expression.expression)) - case FragmentInput(_, FragmentRef(typ, params, _), _, _, _) => - ("Type", typ) :: params.map(p => (p.name, p.expression.expression)) - case FragmentInputDefinition(_, parameters, _) => parameters.map(p => p.name -> p.typ.refClazzName) - case FragmentOutputDefinition(_, outputName, fields, _) => - ("Output name", outputName) :: fields.map(p => p.name -> p.expression.expression) - case Variable(_, name, expr, _) => (name -> expr.expression) :: Nil - case VariableBuilder(_, name, fields, _) => - ("Variable name", name) :: fields.map(p => p.name -> p.expression.expression) - case Join(_, output, typ, parameters, branch, _) => - ("Type", typ) :: ("Output", output.getOrElse("")) :: - parameters.map(p => p.name -> p.expression.expression) ++ branch.flatMap(bp => - bp.parameters.map(p => s"${bp.branchId} - ${p.name}" -> p.expression.expression) - ) - case Split(_, _) => ("No parameters", "") :: Nil - // This should not happen in properly resolved scenario... - case _: BranchEndData => throw new IllegalArgumentException("Should not happen during PDF export") - case _: FragmentUsageOutput => throw new IllegalArgumentException("Should not happen during PDF export") - } - val data = node.additionalFields - .flatMap(_.description) - .map(naf => ("Description", naf)) - .toList ++ nodeData - if (data.isEmpty) { - NodeSeq.Empty - } else { - - - {node.getClass.getSimpleName}{node.id} - - - - - - { - data.map { case (key, value) => - - - - {key} - - - - - {addEmptySpace(value)} - - - - } - } - -
-
- } - - } - - // we want to be able to break line for these characters. it's not really perfect solution for long, complex expressions, - // but should handle most of the cases../ - private def addEmptySpace(str: String) = List(")", ".", "(") - .foldLeft(str) { (acc, el) => acc.replace(el, el + '\u200b') } - - private def nodesSummary(scenarioGraph: ScenarioGraph) = { - - - Nodes summary - - - - - - - - - Node name - - - Type - - - Description - - - - - { - if (scenarioGraph.nodes.isEmpty) { - - - - } else - scenarioGraph.nodes.map { node => - - - - - {node.id} - - - - - - {node.getClass.getSimpleName} - - - - - {node.additionalFields.flatMap(_.description).getOrElse("")} - - - - } - } - -
-
- } - - private def attachments(processActivity: ProcessActivity) = if (processActivity.attachments.isEmpty) { - - } else { - - - Attachments - - - - - - - - - - - Date - - - Author - - - File name - - - - - { - processActivity.attachments - .sortBy(_.createDate) - .map(attachment => - - - - {format(attachment.createDate)} - - - - - {attachment.user} - - - - - {attachment.fileName} - - - ) - } - -
-
- } - -} +// todo NU-1772 in progress +//package pl.touk.nussknacker.ui.util +// +//import java.io._ +//import java.net.URI +//import java.nio.charset.StandardCharsets +//import java.time.{Instant, ZoneId} +//import java.time.format.DateTimeFormatter +//import javax.xml.transform.TransformerFactory +//import javax.xml.transform.sax.SAXResult +//import javax.xml.transform.stream.StreamSource +//import com.typesafe.scalalogging.LazyLogging +//import org.apache.commons.io.IOUtils +//import org.apache.fop.apps.FopConfParser +//import org.apache.fop.apps.io.ResourceResolverFactory +//import org.apache.xmlgraphics.util.MimeConstants +//import pl.touk.nussknacker.engine.api.graph.ScenarioGraph +//import pl.touk.nussknacker.engine.graph.node._ +//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.process.repository.ScenarioWithDetailsEntity +// +//import scala.xml.{Elem, NodeSeq, XML} +// +//object PdfExporter extends LazyLogging { +// +// private val fopFactory = new FopConfParser( +// getClass.getResourceAsStream("/fop/config.xml"), +// new URI("http://touk.pl"), +// ResourceResolverFactory.createDefaultResourceResolver +// ).getFopFactoryBuilder.build +// +// def exportToPdf( +// svg: String, +// processDetails: ScenarioWithDetailsEntity[ScenarioGraph], +// processActivity: ProcessActivity +// ): Array[Byte] = { +// +// // initFontsIfNeeded is invoked every time to make sure that /tmp content is not deleted +// initFontsIfNeeded() +// // FIXME: cannot render polish signs..., better to strip them than not render anything... +// // \u00A0 - non-breaking space in not ASCII :)... +// val fopXml = prepareFopXml( +// svg.replaceAll("\u00A0", " ").replaceAll("[^\\p{ASCII}]", ""), +// processDetails, +// processActivity, +// processDetails.json +// ) +// +// createPdf(fopXml) +// } +// +// // in PDF export we print timezone, to avoid ambiguity +// // TODO: pass client timezone from FE +// private def format(instant: Instant) = { +// val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss [VV]") +// instant.atZone(ZoneId.systemDefault()).format(formatter) +// } +// +// // TODO: this is one nasty hack, is there a better way to make fop read fonts from classpath? +// private def initFontsIfNeeded(): Unit = synchronized { +// val dir = new File("/tmp/fop/fonts") +// dir.mkdirs() +// List( +// "OpenSans-BoldItalic.ttf", +// "OpenSans-Bold.ttf", +// "OpenSans-ExtraBoldItalic.ttf", +// "OpenSans-ExtraBold.ttf", +// "OpenSans-Italic.ttf", +// "OpenSans-LightItalic.ttf", +// "OpenSans-Light.ttf", +// "OpenSans-Regular.ttf", +// "OpenSans-SemiboldItalic.ttf", +// "OpenSans-Semibold.ttf" +// ).filterNot(name => new File(dir, name).exists()).foreach { name => +// IOUtils.copy(getClass.getResourceAsStream(s"/fop/fonts/$name"), new FileOutputStream(new File(dir, name))) +// } +// } +// +// private def createPdf(fopXml: Elem): Array[Byte] = { +// val out = new ByteArrayOutputStream() +// val fop = fopFactory.newFop(MimeConstants.MIME_PDF, out) +// val src = new StreamSource(new ByteArrayInputStream(fopXml.toString().getBytes(StandardCharsets.UTF_8))) +// TransformerFactory.newInstance().newTransformer().transform(src, new SAXResult(fop.getDefaultHandler)) +// out.toByteArray +// } +// +// private def prepareFopXml( +// svg: String, +// processDetails: ScenarioWithDetailsEntity[ScenarioGraph], +// processActivity: ProcessActivity, +// scenarioGraph: ScenarioGraph +// ) = { +// val diagram = XML.loadString(svg) +// val currentVersion = processDetails.history.get.find(_.processVersionId == processDetails.processVersionId).get +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// {processDetails.name} +// ( +// {processDetails.processCategory} +// ) +// +// +// +// Version: +// {processDetails.processVersionId} +// +// +// Saved by +// {currentVersion.user} +// at +// {format(currentVersion.createDate)} +// +// +// {processDetails.description.getOrElse("")} +// +// +// +// {diagram} +// +// +// {nodesSummary(scenarioGraph)} +// Nodes details +// {scenarioGraph.nodes.map(nodeDetails)}{comments(processActivity)}{attachments(processActivity)} +// +// +// +// +// } +// +// private def comments(processActivity: ProcessActivity) = +// +// +// Comments +// +// +// +// +// +// +// +// +// Date +// +// +// Author +// +// +// Comment +// +// +// +// +// { +// if (processActivity.comments.isEmpty) { +// +// +// +// } else +// processActivity.comments.sortBy(_.createDate).map { comment => +// +// +// +// {format(comment.createDate)} +// +// +// +// +// {comment.user} +// +// +// +// +// {comment.content} +// +// +// +// } +// } +// +//
+//
+//
+// +// private def nodeDetails(node: NodeData) = { +// val nodeData = node match { +// case Source(_, SourceRef(typ, params), _) => ("Type", typ) :: params.map(p => (p.name, p.expression.expression)) +// case Filter(_, expression, _, _) => List(("Expression", expression.expression)) +// case Enricher(_, ServiceRef(typ, params), output, _) => +// ("Type", typ) :: ("Output", output) :: params.map(p => (p.name, p.expression.expression)) +// // TODO: what about Swtich?? +// case Switch(_, expression, exprVal, _) => expression.map(e => ("Expression", e.expression)).toList +// case Processor(_, ServiceRef(typ, params), _, _) => +// ("Type", typ) :: params.map(p => (p.name, p.expression.expression)) +// case Sink(_, SinkRef(typ, params), _, _, _) => ("Type", typ) :: params.map(p => (p.name, p.expression.expression)) +// case CustomNode(_, output, typ, params, _) => +// ("Type", typ) :: ("Output", output.getOrElse("")) :: params.map(p => (p.name, p.expression.expression)) +// case FragmentInput(_, FragmentRef(typ, params, _), _, _, _) => +// ("Type", typ) :: params.map(p => (p.name, p.expression.expression)) +// case FragmentInputDefinition(_, parameters, _) => parameters.map(p => p.name -> p.typ.refClazzName) +// case FragmentOutputDefinition(_, outputName, fields, _) => +// ("Output name", outputName) :: fields.map(p => p.name -> p.expression.expression) +// case Variable(_, name, expr, _) => (name -> expr.expression) :: Nil +// case VariableBuilder(_, name, fields, _) => +// ("Variable name", name) :: fields.map(p => p.name -> p.expression.expression) +// case Join(_, output, typ, parameters, branch, _) => +// ("Type", typ) :: ("Output", output.getOrElse("")) :: +// parameters.map(p => p.name -> p.expression.expression) ++ branch.flatMap(bp => +// bp.parameters.map(p => s"${bp.branchId} - ${p.name}" -> p.expression.expression) +// ) +// case Split(_, _) => ("No parameters", "") :: Nil +// // This should not happen in properly resolved scenario... +// case _: BranchEndData => throw new IllegalArgumentException("Should not happen during PDF export") +// case _: FragmentUsageOutput => throw new IllegalArgumentException("Should not happen during PDF export") +// } +// val data = node.additionalFields +// .flatMap(_.description) +// .map(naf => ("Description", naf)) +// .toList ++ nodeData +// if (data.isEmpty) { +// NodeSeq.Empty +// } else { +// +// +// {node.getClass.getSimpleName}{node.id} +// +// +// +// +// +// { +// data.map { case (key, value) => +// +// +// +// {key} +// +// +// +// +// {addEmptySpace(value)} +// +// +// +// } +// } +// +//
+//
+// } +// +// } +// +// // we want to be able to break line for these characters. it's not really perfect solution for long, complex expressions, +// // but should handle most of the cases../ +// private def addEmptySpace(str: String) = List(")", ".", "(") +// .foldLeft(str) { (acc, el) => acc.replace(el, el + '\u200b') } +// +// private def nodesSummary(scenarioGraph: ScenarioGraph) = { +// +// +// Nodes summary +// +// +// +// +// +// +// +// +// Node name +// +// +// Type +// +// +// Description +// +// +// +// +// { +// if (scenarioGraph.nodes.isEmpty) { +// +// +// +// } else +// scenarioGraph.nodes.map { node => +// +// +// +// +// {node.id} +// +// +// +// +// +// {node.getClass.getSimpleName} +// +// +// +// +// {node.additionalFields.flatMap(_.description).getOrElse("")} +// +// +// +// } +// } +// +//
+//
+// } +// +// private def attachments(processActivity: ProcessActivity) = if (processActivity.attachments.isEmpty) { +// +// } else { +// +// +// Attachments +// +// +// +// +// +// +// +// +// +// +// Date +// +// +// Author +// +// +// File name +// +// +// +// +// { +// processActivity.attachments +// .sortBy(_.createDate) +// .map(attachment => +// +// +// +// {format(attachment.createDate)} +// +// +// +// +// {attachment.user} +// +// +// +// +// {attachment.fileName} +// +// +// ) +// } +// +//
+//
+// } +// +//} 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 091deef979d..05ee7cd647e 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 @@ -49,6 +49,7 @@ 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} @@ -91,7 +92,7 @@ trait NuResourcesTest protected val actionRepository: DbProcessActionRepository = newActionProcessRepository(testDbRef) - protected val processActivityRepository: DbProcessActivityRepository = newProcessActivityRepository(testDbRef) + protected val scenarioActivityRepository: ScenarioActivityRepository = newScenarioActivityRepository(testDbRef) protected val processChangeListener = new TestProcessChangeListener() @@ -170,7 +171,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) @@ -655,9 +656,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 @@ -670,7 +672,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/utils/domain/ScenarioHelper.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/domain/ScenarioHelper.scala index 7ae550d4e4f..34d0d94938d 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,6 +17,7 @@ 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 @@ -32,13 +33,12 @@ private[test] class ScenarioHelper(dbRef: DbRef, designerConfig: Config)(implici private val actionRepository: DbProcessActionRepository = new DbProcessActionRepository( dbRef, - new CommentRepository(dbRef), mapProcessingTypeDataProvider(Map("engine-version" -> "0.1")) ) with DbioRepository private val writeScenarioRepository: DBProcessRepository = new DBProcessRepository( dbRef, - new CommentRepository(dbRef), + new DbScenarioActivityRepository(dbRef), mapProcessingTypeDataProvider(1) ) @@ -131,7 +131,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, @@ -145,7 +145,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) ) @@ -153,7 +153,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 2ad2970ea7e..301c677293f 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 @@ -143,7 +144,7 @@ object TestFactory { def newDummyDBIOActionRunner(): DBIOActionRunner = newDBIOActionRunner(dummyDbRef) - def newCommentRepository(dbRef: DbRef) = new CommentRepository(dbRef) + def newScenarioActivityRepository(dbRef: DbRef) = new DbScenarioActivityRepository(dbRef) def newFutureFetchingScenarioRepository(dbRef: DbRef) = new DBFetchingProcessRepository[Future](dbRef, newActionProcessRepository(dbRef)) with BasicRepository @@ -154,8 +155,8 @@ object TestFactory { def newWriteProcessRepository(dbRef: DbRef, modelVersions: Option[Int] = Some(1)) = new DBProcessRepository( dbRef, - newCommentRepository(dbRef), - mapProcessingTypeDataProvider(modelVersions.map(Streaming.stringify -> _).toList: _*) + newScenarioActivityRepository(dbRef), + mapProcessingTypeDataProvider(modelVersions.map(Streaming.stringify -> _).toList: _*), ) def newDummyWriteProcessRepository(): DBProcessRepository = @@ -176,15 +177,12 @@ object TestFactory { def newActionProcessRepository(dbRef: DbRef) = new DbProcessActionRepository( dbRef, - newCommentRepository(dbRef), mapProcessingTypeDataProvider(Streaming.stringify -> buildInfo) ) with DbioRepository def newDummyActionRepository(): DbProcessActionRepository = 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..4fa6b55cdea 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 @@ -145,8 +145,8 @@ class ManagementResourcesSpec val expectedDeployComment = "Deployment: deployComment" val expectedStopComment = "Stop: cancelComment" getActivity(ProcessTestData.sampleScenario.name) ~> check { - val comments = responseAs[ProcessActivity].comments.sortBy(_.id) - comments.map(_.content) shouldBe List(expectedDeployComment, expectedStopComment) + val comments = responseAs[Dtos.ScenarioActivities].activities + // todomgw comments.map(_.content) shouldBe List(expectedDeployComment, expectedStopComment) val firstCommentId :: secondCommentId :: Nil = comments.map(_.id) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala index 49ade0644d3..7c0bc88ff69 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala @@ -1,5 +1,7 @@ package pl.touk.nussknacker.ui.api +import akka.actor.ActorSystem +import akka.stream.Materializer import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers import org.scalatest.prop.TableDrivenPropertyChecks._ @@ -8,6 +10,7 @@ import pl.touk.nussknacker.security.AuthCredentials.PassedAuthCredentials import pl.touk.nussknacker.test.utils.domain.ReflectionBasedUtils import pl.touk.nussknacker.test.utils.{InvalidExample, OpenAPIExamplesValidator, OpenAPISchemaComponents} import pl.touk.nussknacker.ui.security.api.AuthManager.ImpersonationConsideringInputEndpoint +import pl.touk.nussknacker.ui.server.{AkkaHttpBasedTapirStreamEndpointProvider, TapirStreamEndpointProvider} import pl.touk.nussknacker.ui.services.NuDesignerExposedApiHttpService import pl.touk.nussknacker.ui.util.Project import sttp.apispec.openapi.circe.yaml.RichOpenAPI @@ -15,6 +18,7 @@ import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter import sttp.tapir.{Endpoint, EndpointInput, auth} import java.lang.reflect.{Method, Modifier} +import scala.concurrent.Await import scala.util.Try // if the test fails it probably means that you should regenerate the Nu Designer OpenAPI document @@ -138,30 +142,46 @@ class NuDesignerApiAvailableToExposeYamlSpec extends AnyFunSuite with Matchers { object NuDesignerApiAvailableToExpose { - def generateOpenApiYaml: String = { - val endpoints = findApiEndpointsClasses().flatMap(findEndpointsInClass) + def generateOpenApiYaml: String = withStreamProvider { streamProvider => + val endpoints = findApiEndpointsClasses().flatMap(findEndpointsInClass(streamProvider)) val docs = OpenAPIDocsInterpreter(NuDesignerExposedApiHttpService.openAPIDocsOptions).toOpenAPI( es = endpoints, title = NuDesignerExposedApiHttpService.openApiDocumentTitle, version = "" ) - docs.toYaml } + private def withStreamProvider[T](handle: TapirStreamEndpointProvider => T): T = { + val actorSystem: ActorSystem = ActorSystem() + val mat: Materializer = Materializer(actorSystem) + val streamProvider: TapirStreamEndpointProvider = new AkkaHttpBasedTapirStreamEndpointProvider()(mat) + val result = handle(streamProvider) + Await.result(actorSystem.terminate(), scala.concurrent.duration.Duration.Inf) + result + } + private def findApiEndpointsClasses() = { ReflectionBasedUtils.findSubclassesOf[BaseEndpointDefinitions]("pl.touk.nussknacker.ui.api") } - private def findEndpointsInClass(clazz: Class[_ <: BaseEndpointDefinitions]) = { - val endpointDefinitions = createInstanceOf(clazz) + private def findEndpointsInClass( + streamEndpointProvider: TapirStreamEndpointProvider + )( + clazz: Class[_ <: BaseEndpointDefinitions] + ) = { + val endpointDefinitions = createInstanceOf(streamEndpointProvider)(clazz) clazz.getDeclaredMethods.toList .filter(isEndpointMethod) .sortBy(_.getName) .map(instantiateEndpointDefinition(endpointDefinitions, _)) } - private def createInstanceOf(clazz: Class[_ <: BaseEndpointDefinitions]) = { + private def createInstanceOf( + streamEndpointProvider: TapirStreamEndpointProvider, + )( + clazz: Class[_ <: BaseEndpointDefinitions], + ) = { val basicAuth = auth .basic[Option[String]]() .map(_.map(PassedAuthCredentials))(_.map(_.value)) @@ -173,6 +193,10 @@ object NuDesignerApiAvailableToExpose { Try(clazz.getDeclaredConstructor()) .map(_.newInstance()) } + .orElse { + Try(clazz.getConstructor(classOf[EndpointInput[PassedAuthCredentials]], classOf[TapirStreamEndpointProvider])) + .map(_.newInstance(basicAuth, streamEndpointProvider)) + } .getOrElse( throw new IllegalStateException( s"Class ${clazz.getName} is required to have either one parameter constructor or constructor without parameters" 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..0c22d82adf2 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 @@ -1,173 +1,174 @@ -package pl.touk.nussknacker.ui.api - -import akka.http.scaladsl.model.{ContentTypeRange, ContentTypes, HttpEntity, StatusCodes} -import akka.http.scaladsl.server.Route -import akka.http.scaladsl.testkit.ScalatestRouteTest -import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} -import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport -import io.circe.syntax._ -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.matchers.should.Matchers -import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Inside, OptionValues} -import pl.touk.nussknacker.engine.api.graph.ScenarioGraph -import pl.touk.nussknacker.engine.api.{ProcessAdditionalFields, StreamMetaData} -import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.engine.marshall.ProcessMarshaller -import pl.touk.nussknacker.restmodel.validation.ScenarioGraphWithValidationResult -import pl.touk.nussknacker.test.PatientScalaFutures -import pl.touk.nussknacker.test.utils.domain.TestFactory.{asAdmin, processResolverByProcessingType, withAllPermissions} -import pl.touk.nussknacker.test.base.it.NuResourcesTest -import pl.touk.nussknacker.test.utils.domain.ProcessTestData -import pl.touk.nussknacker.ui.process.marshall.CanonicalProcessConverter -import pl.touk.nussknacker.ui.util.MultipartUtils - -class ProcessesExportImportResourcesSpec - extends AnyFunSuite - with ScalatestRouteTest - with Matchers - with Inside - with FailFastCirceSupport - with PatientScalaFutures - with OptionValues - with BeforeAndAfterEach - with BeforeAndAfterAll - with NuResourcesTest { - - import akka.http.scaladsl.server.RouteConcatenation._ - - private implicit final val string: FromEntityUnmarshaller[String] = - Unmarshaller.stringUnmarshaller.forContentTypes(ContentTypeRange.*) - - private val processesExportResources = new ProcessesExportResources( - futureFetchingScenarioRepository, - processService, - processActivityRepository, - processResolverByProcessingType - ) - - private val routeWithAllPermissions = withAllPermissions(processesExportResources) ~ - withAllPermissions(processesRoute) - private val adminRoute = asAdmin(processesExportResources) ~ asAdmin(processesRoute) - - test("export process from scenarioGraph") { - val scenarioGraphToExport = ProcessTestData.sampleScenarioGraph - createEmptyProcess(ProcessTestData.sampleProcessName) - - Post( - s"/processesExport/${ProcessTestData.sampleProcessName}", - scenarioGraphToExport - ) ~> routeWithAllPermissions ~> check { - status shouldEqual StatusCodes.OK - val exported = responseAs[String] - val processDetails = ProcessMarshaller.fromJson(exported).toOption.get - - processDetails shouldBe CanonicalProcessConverter.fromScenarioGraph( - scenarioGraphToExport, - ProcessTestData.sampleProcessName - ) - } - } - - test("export process and import it (as common user)") { - runImportExportTest(routeWithAllPermissions) - } - - test("export process and import it (as admin)") { - runImportExportTest(adminRoute) - } - - private def runImportExportTest(route: Route): Unit = { - val scenarioGraphToSave = ProcessTestData.sampleScenarioGraph - saveProcess(scenarioGraphToSave) { - status shouldEqual StatusCodes.OK - } - - Get(s"/processesExport/${ProcessTestData.sampleProcessName}/2") ~> route ~> check { - val response = responseAs[String] - val processDetails = ProcessMarshaller.fromJson(response).toOption.get - assertProcessPrettyPrinted(response, processDetails) - - val modified = processDetails.copy(metaData = - processDetails.metaData.withTypeSpecificData(typeSpecificData = StreamMetaData(Some(987))) - ) - val multipartForm = MultipartUtils.prepareMultiPart(modified.asJson.spaces2, "process") - Post(s"/processes/import/${ProcessTestData.sampleProcessName}", multipartForm) ~> route ~> check { - status shouldEqual StatusCodes.OK - val imported = responseAs[ScenarioGraphWithValidationResult] - imported.scenarioGraph.properties.typeSpecificProperties.asInstanceOf[StreamMetaData].parallelism shouldBe Some( - 987 - ) - imported.scenarioGraph.nodes shouldBe scenarioGraphToSave.nodes - } - } - } - - test("export process in new version") { - val description = "alamakota" - val scenarioGraphToSave = ProcessTestData.sampleScenarioGraph - val processWithDescription = scenarioGraphToSave.copy(properties = - scenarioGraphToSave.properties.copy(additionalFields = - ProcessAdditionalFields(Some(description), Map.empty, StreamMetaData.typeName) - ) - ) - - saveProcess(scenarioGraphToSave) { - status shouldEqual StatusCodes.OK - } - updateProcess(processWithDescription) { - status shouldEqual StatusCodes.OK - } - - Get(s"/processesExport/${ProcessTestData.sampleProcessName}/2") ~> routeWithAllPermissions ~> check { - val response = responseAs[String] - response shouldNot include(description) - assertProcessPrettyPrinted(response, scenarioGraphToSave) - } - - Get(s"/processesExport/${ProcessTestData.sampleProcessName}/3") ~> routeWithAllPermissions ~> check { - val latestProcessVersion = io.circe.parser.parse(responseAs[String]) - latestProcessVersion.toOption.get.spaces2 should include(description) - - Get(s"/processesExport/${ProcessTestData.sampleProcessName}") ~> routeWithAllPermissions ~> check { - io.circe.parser.parse(responseAs[String]) shouldBe latestProcessVersion - } - - } - - } - - test("export pdf") { - val scenarioGraphToSave = ProcessTestData.sampleScenarioGraph - saveProcess(scenarioGraphToSave) { - status shouldEqual StatusCodes.OK - - val testSvg = "\n " + - "\n " + - "\n" - - Post( - s"/processesExport/pdf/${ProcessTestData.sampleProcessName}/2", - HttpEntity(testSvg) - ) ~> routeWithAllPermissions ~> check { - - status shouldEqual StatusCodes.OK - contentType shouldEqual ContentTypes.`application/octet-stream` - // just simple sanity check that it's really pdf... - responseAs[String] should startWith("%PDF") - } - } - - } - - private def assertProcessPrettyPrinted(response: String, expectedProcess: CanonicalProcess): Unit = { - response shouldBe expectedProcess.asJson.spaces2 - } - - private def assertProcessPrettyPrinted(response: String, process: ScenarioGraph): Unit = { - assertProcessPrettyPrinted( - response, - CanonicalProcessConverter.fromScenarioGraph(process, ProcessTestData.sampleProcessName) - ) - } - -} +// todo NU-1772 in progress +//package pl.touk.nussknacker.ui.api +// +//import akka.http.scaladsl.model.{ContentTypeRange, ContentTypes, HttpEntity, StatusCodes} +//import akka.http.scaladsl.server.Route +//import akka.http.scaladsl.testkit.ScalatestRouteTest +//import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} +//import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport +//import io.circe.syntax._ +//import org.scalatest.funsuite.AnyFunSuite +//import org.scalatest.matchers.should.Matchers +//import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Inside, OptionValues} +//import pl.touk.nussknacker.engine.api.graph.ScenarioGraph +//import pl.touk.nussknacker.engine.api.{ProcessAdditionalFields, StreamMetaData} +//import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess +//import pl.touk.nussknacker.engine.marshall.ProcessMarshaller +//import pl.touk.nussknacker.restmodel.validation.ScenarioGraphWithValidationResult +//import pl.touk.nussknacker.test.PatientScalaFutures +//import pl.touk.nussknacker.test.utils.domain.TestFactory.{asAdmin, processResolverByProcessingType, withAllPermissions} +//import pl.touk.nussknacker.test.base.it.NuResourcesTest +//import pl.touk.nussknacker.test.utils.domain.ProcessTestData +//import pl.touk.nussknacker.ui.process.marshall.CanonicalProcessConverter +//import pl.touk.nussknacker.ui.util.MultipartUtils +// +//class ProcessesExportImportResourcesSpec +// extends AnyFunSuite +// with ScalatestRouteTest +// with Matchers +// with Inside +// with FailFastCirceSupport +// with PatientScalaFutures +// with OptionValues +// with BeforeAndAfterEach +// with BeforeAndAfterAll +// with NuResourcesTest { +// +// import akka.http.scaladsl.server.RouteConcatenation._ +// +// private implicit final val string: FromEntityUnmarshaller[String] = +// Unmarshaller.stringUnmarshaller.forContentTypes(ContentTypeRange.*) +// +// private val processesExportResources = new ProcessesExportResources( +// futureFetchingScenarioRepository, +// processService, +// processActivityRepository, +// processResolverByProcessingType +// ) +// +// private val routeWithAllPermissions = withAllPermissions(processesExportResources) ~ +// withAllPermissions(processesRoute) +// private val adminRoute = asAdmin(processesExportResources) ~ asAdmin(processesRoute) +// +// test("export process from scenarioGraph") { +// val scenarioGraphToExport = ProcessTestData.sampleScenarioGraph +// createEmptyProcess(ProcessTestData.sampleProcessName) +// +// Post( +// s"/processesExport/${ProcessTestData.sampleProcessName}", +// scenarioGraphToExport +// ) ~> routeWithAllPermissions ~> check { +// status shouldEqual StatusCodes.OK +// val exported = responseAs[String] +// val processDetails = ProcessMarshaller.fromJson(exported).toOption.get +// +// processDetails shouldBe CanonicalProcessConverter.fromScenarioGraph( +// scenarioGraphToExport, +// ProcessTestData.sampleProcessName +// ) +// } +// } +// +// test("export process and import it (as common user)") { +// runImportExportTest(routeWithAllPermissions) +// } +// +// test("export process and import it (as admin)") { +// runImportExportTest(adminRoute) +// } +// +// private def runImportExportTest(route: Route): Unit = { +// val scenarioGraphToSave = ProcessTestData.sampleScenarioGraph +// saveProcess(scenarioGraphToSave) { +// status shouldEqual StatusCodes.OK +// } +// +// Get(s"/processesExport/${ProcessTestData.sampleProcessName}/2") ~> route ~> check { +// val response = responseAs[String] +// val processDetails = ProcessMarshaller.fromJson(response).toOption.get +// assertProcessPrettyPrinted(response, processDetails) +// +// val modified = processDetails.copy(metaData = +// processDetails.metaData.withTypeSpecificData(typeSpecificData = StreamMetaData(Some(987))) +// ) +// val multipartForm = MultipartUtils.prepareMultiPart(modified.asJson.spaces2, "process") +// Post(s"/processes/import/${ProcessTestData.sampleProcessName}", multipartForm) ~> route ~> check { +// status shouldEqual StatusCodes.OK +// val imported = responseAs[ScenarioGraphWithValidationResult] +// imported.scenarioGraph.properties.typeSpecificProperties.asInstanceOf[StreamMetaData].parallelism shouldBe Some( +// 987 +// ) +// imported.scenarioGraph.nodes shouldBe scenarioGraphToSave.nodes +// } +// } +// } +// +// test("export process in new version") { +// val description = "alamakota" +// val scenarioGraphToSave = ProcessTestData.sampleScenarioGraph +// val processWithDescription = scenarioGraphToSave.copy(properties = +// scenarioGraphToSave.properties.copy(additionalFields = +// ProcessAdditionalFields(Some(description), Map.empty, StreamMetaData.typeName) +// ) +// ) +// +// saveProcess(scenarioGraphToSave) { +// status shouldEqual StatusCodes.OK +// } +// updateProcess(processWithDescription) { +// status shouldEqual StatusCodes.OK +// } +// +// Get(s"/processesExport/${ProcessTestData.sampleProcessName}/2") ~> routeWithAllPermissions ~> check { +// val response = responseAs[String] +// response shouldNot include(description) +// assertProcessPrettyPrinted(response, scenarioGraphToSave) +// } +// +// Get(s"/processesExport/${ProcessTestData.sampleProcessName}/3") ~> routeWithAllPermissions ~> check { +// val latestProcessVersion = io.circe.parser.parse(responseAs[String]) +// latestProcessVersion.toOption.get.spaces2 should include(description) +// +// Get(s"/processesExport/${ProcessTestData.sampleProcessName}") ~> routeWithAllPermissions ~> check { +// io.circe.parser.parse(responseAs[String]) shouldBe latestProcessVersion +// } +// +// } +// +// } +// +// test("export pdf") { +// val scenarioGraphToSave = ProcessTestData.sampleScenarioGraph +// saveProcess(scenarioGraphToSave) { +// status shouldEqual StatusCodes.OK +// +// val testSvg = "\n " + +// "\n " + +// "\n" +// +// Post( +// s"/processesExport/pdf/${ProcessTestData.sampleProcessName}/2", +// HttpEntity(testSvg) +// ) ~> routeWithAllPermissions ~> check { +// +// status shouldEqual StatusCodes.OK +// contentType shouldEqual ContentTypes.`application/octet-stream` +// // just simple sanity check that it's really pdf... +// responseAs[String] should startWith("%PDF") +// } +// } +// +// } +// +// private def assertProcessPrettyPrinted(response: String, expectedProcess: CanonicalProcess): Unit = { +// response shouldBe expectedProcess.asJson.spaces2 +// } +// +// private def assertProcessPrettyPrinted(response: String, process: ScenarioGraph): Unit = { +// assertProcessPrettyPrinted( +// response, +// CanonicalProcessConverter.fromScenarioGraph(process, ProcessTestData.sampleProcessName) +// ) +// } +// +//} 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 0cd6bcd4fc7..48acd94a540 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 @@ -45,7 +45,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 @@ -609,8 +608,9 @@ class ProcessesResourcesSpec } getActivity(processName) ~> check { - val comments = responseAs[ProcessActivity].comments - comments.loneElement.content shouldBe comment +// todo NU-1772 in progress +// val comments = responseAs[ProcessActivity].comments +// comments.loneElement.content shouldBe comment } } 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 23475150e28..35eecf5b16c 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,8 +6,8 @@ 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.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 @@ -34,7 +34,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) private lazy val scenarioRepository = TestFactory.newFetchingProcessRepository(testDbRef) @@ -47,7 +47,7 @@ abstract class InitializationOnDbItSpec it should "migrate processes" in { saveSampleProcess() - Initialization.init(migrations, testDbRef, scenarioRepository, commentRepository, "env1") + Initialization.init(migrations, testDbRef, scenarioRepository, scenarioActivityRepository, "env1") dbioRunner .runInTransaction( @@ -66,7 +66,7 @@ abstract class InitializationOnDbItSpec saveSampleProcess(ProcessName(s"id$id")) } - Initialization.init(migrations, testDbRef, scenarioRepository, commentRepository, "env1") + Initialization.init(migrations, testDbRef, scenarioRepository, scenarioActivityRepository, "env1") dbioRunner .runInTransaction( @@ -85,7 +85,7 @@ abstract class InitializationOnDbItSpec mapProcessingTypeDataProvider("streaming" -> new TestMigrations(1, 2, 5)), testDbRef, scenarioRepository, - commentRepository, + scenarioActivityRepository, "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..174ae740bea 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 @@ -62,12 +62,10 @@ class NotificationServiceTest 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 actionRepository = new DbProcessActionRepository( testDbRef, - commentRepository, ProcessingTypeDataProvider.withEmptyCombinedData(Map.empty) ) 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..14d9c029d30 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,31 @@ 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.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 +49,33 @@ class ScenarioAttachmentServiceSpec extends AnyFunSuite with Matchers with Scala } -private object TestProcessActivityRepository extends ProcessActivityRepository { +private object TestProcessActivityRepository extends ScenarioActivityRepository { + + override def fetchActivities(scenarioId: ProcessId)(implicit user: LoggedUser): 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 editComment(scenarioActivityId: ScenarioActivityId, comment: String)( + implicit user: LoggedUser + ): DB[Either[ScenarioActivityRepository.ModifyCommentError, Unit]] = ??? - override def addComment(processId: ProcessId, processVersionId: VersionId, comment: Comment)( - implicit ec: ExecutionContext, - loggedUser: LoggedUser - ): Future[Unit] = ??? + override def deleteComment(scenarioActivityId: ScenarioActivityId)( + implicit user: LoggedUser + ): DB[Either[ScenarioActivityRepository.ModifyCommentError, Unit]] = ??? - override def deleteComment(commentId: Long)(implicit ec: ExecutionContext): Future[Either[Exception, Unit]] = ??? + override def addAttachment(attachmentToAdd: ScenarioAttachmentService.AttachmentToAdd)( + implicit user: LoggedUser + ): DB[ScenarioActivityId] = + DBIO.successful(ScenarioActivityId.random) - override def findActivity(processId: ProcessId)( - implicit ec: ExecutionContext - ): Future[DbProcessActivityRepository.ProcessActivity] = ??? + override def findAttachment(scenarioId: ProcessId, attachmentId: Long): DB[Option[AttachmentEntityData]] = ??? - override def addAttachment( - attachmentToAdd: ScenarioAttachmentService.AttachmentToAdd - )(implicit ec: ExecutionContext, loggedUser: LoggedUser): Future[Unit] = Future.successful(()) + override def findActivity(processId: ProcessId): DB[String] = ??? - override def findAttachment(attachmentId: Long, scenarioId: ProcessId)( - implicit ec: ExecutionContext - ): Future[Option[AttachmentEntityData]] = - ??? + override def getActivityStats: DB[Map[String, Int]] = ??? - 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..6a104740bcb 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 @@ -30,12 +30,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 @@ -70,7 +65,7 @@ class DeploymentServiceSpec private val futureFetchingProcessRepository = newFutureFetchingScenarioRepository(testDbRef) private val writeProcessRepository = newWriteProcessRepository(testDbRef) private val actionRepository = newActionProcessRepository(testDbRef) - private val activityRepository = newProcessActivityRepository(testDbRef) + private val activityRepository = newScenarioActivityRepository(testDbRef) private val processingTypeDataProvider: ProcessingTypeDataProvider[DeploymentManager, Nothing] = new ProcessingTypeDataProvider[DeploymentManager, Nothing] { @@ -335,7 +330,8 @@ 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 + // todo NU-1772 in progress + // dbioRunner.run(activityRepository.findActivity(processId.id)).futureValue.comments should have length 1 deploymentManager.withEmptyProcessState(processName) { val stateAfterJobRetention = @@ -1025,7 +1021,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/repository/DBFetchingProcessRepositorySpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/repository/DBFetchingProcessRepositorySpec.scala index 5338b737c3a..8059c6e894a 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 @@ -16,13 +16,13 @@ import pl.touk.nussknacker.test.utils.domain.TestFactory.mapProcessingTypeDataPr import pl.touk.nussknacker.test.utils.domain.{ProcessTestData, TestFactory} 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 @@ -40,10 +40,10 @@ class DBFetchingProcessRepositorySpec private val dbioRunner = DBIOActionRunner(testDbRef) - private val commentRepository = new CommentRepository(testDbRef) + private val activities = new DbScenarioActivityRepository(testDbRef) private val writingRepo = - new DBProcessRepository(testDbRef, commentRepository, mapProcessingTypeDataProvider("Streaming" -> 0)) { + new DBProcessRepository(testDbRef, activities, mapProcessingTypeDataProvider("Streaming" -> 0)) { override protected def now: Instant = currentTime } @@ -52,14 +52,11 @@ class DBFetchingProcessRepositorySpec private val actions = new DbProcessActionRepository( testDbRef, - commentRepository, ProcessingTypeDataProvider.withEmptyCombinedData(Map.empty) ) private val fetching = DBFetchingProcessRepository.createFutureRepository(testDbRef, actions) - private val activities = DbProcessActivityRepository(testDbRef, commentRepository) - private implicit val user: LoggedUser = TestFactory.adminUser() test("fetch processes for category") { @@ -150,14 +147,15 @@ class DBFetchingProcessRepositorySpec renameProcess(oldName, newName) - val comments = fetching - .fetchProcessId(newName) - .flatMap(v => activities.findActivity(v.get).map(_.comments)) - .futureValue - - atLeast(1, comments) should matchPattern { - case Comment(_, VersionId(1L), "Rename: [oldName] -> [newName]", user.username, _) => - } +// todo NU-1772 in progress +// val comments = fetching +// .fetchProcessId(newName) +// .flatMap(v => activities.findActivity(v.get).map(_.comments)) +// .futureValue +// +// atLeast(1, comments) should matchPattern { +// case Comment(_, VersionId(1L), "Rename: [oldName] -> [newName]", user.username, _) => +// } } test("should prevent rename to existing name") { 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..df49b47f848 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 @@ -1,97 +1,98 @@ -package pl.touk.nussknacker.ui.util - -import org.apache.commons.io.IOUtils -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import pl.touk.nussknacker.engine.api.StreamMetaData -import pl.touk.nussknacker.engine.api.graph.{ProcessProperties, ScenarioGraph} -import pl.touk.nussknacker.engine.api.process.{ProcessName, ScenarioVersion, VersionId} -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.process.marshall.CanonicalProcessConverter -import pl.touk.nussknacker.ui.process.repository.DbProcessActivityRepository.{Comment, ProcessActivity} - -import java.io.FileOutputStream -import java.time.Instant - -class PdfExporterSpec extends AnyFlatSpec with Matchers { - - private val history = List( - ScenarioVersion(VersionId.initialVersionId, Instant.now(), "Zenon Wojciech", Option.empty, List.empty) - ) - - it should "export process to " in { - val scenarioGraph: ScenarioGraph = - CanonicalProcessConverter.toScenarioGraph(ProcessTestData.sampleScenario) - val graphWithFilterWithComment: ScenarioGraph = scenarioGraph.copy(nodes = scenarioGraph.nodes.map { - case a: Filter => - a.copy(additionalFields = Some(UserDefinedAdditionalNodeFields(Some("mój wnikliwy komętaż"), None))) - case a => a - }) - - val details = createDetails(graphWithFilterWithComment) - - val comments = (1 to 29) - .map(commentId => - Comment( - commentId, - details.processVersionId, - "Jakiś taki dziwny ten proces??", - "Wacław Wójcik", - Instant.now() - ) - ) - .toList - - val activities = ProcessActivity(comments, List()) - - val svg: String = ResourceLoader.load("/svg/svgTest.svg") - val exported = PdfExporter.exportToPdf(svg, details, activities) - - IOUtils.write(exported, new FileOutputStream("/tmp/out.pdf")) - } - - it should "export empty process to " in { - val scenarioGraph: ScenarioGraph = ScenarioGraph( - ProcessProperties(StreamMetaData()), - List(), - List(), - ) - - val details = createDetails(scenarioGraph) - - val activities = ProcessActivity(List(), List()) - val svg: String = ResourceLoader.load("/svg/svgTest.svg") - val exported = PdfExporter.exportToPdf(svg, details, activities) - - IOUtils.write(exported, new FileOutputStream("/tmp/empty.pdf")) - } - - it should "not allow entities in provided SVG" in { - val scenarioGraph: ScenarioGraph = ScenarioGraph( - ProcessProperties(StreamMetaData()), - List(), - List(), - ) - - val details = createDetails(scenarioGraph) - - val activities = ProcessActivity(List(), List()) - val svg: String = ResourceLoader.load("/svg/unsafe.svg") - val ex = intercept[Exception] { - PdfExporter.exportToPdf(svg, details, activities) - } - ex.getMessage should include("DOCTYPE is disallowed") - } - - private def createDetails(scenarioGraph: ScenarioGraph) = TestProcessUtil.wrapWithScenarioDetailsEntity( - ProcessName("My process"), - scenarioGraph = Some(scenarioGraph), - description = Some( - "My fancy description, which is quite, quite, quite looooooooong. \n And it contains maaaany, maaany strange features..." - ), - history = Some(history) - ) - -} +//todo NU-1772 in progress +//package pl.touk.nussknacker.ui.util +// +//import org.apache.commons.io.IOUtils +//import org.scalatest.flatspec.AnyFlatSpec +//import org.scalatest.matchers.should.Matchers +//import pl.touk.nussknacker.engine.api.StreamMetaData +//import pl.touk.nussknacker.engine.api.graph.{ProcessProperties, ScenarioGraph} +//import pl.touk.nussknacker.engine.api.process.{ProcessName, ScenarioVersion, VersionId} +//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.process.marshall.CanonicalProcessConverter +//import pl.touk.nussknacker.ui.process.repository.DbProcessActivityRepository.{Comment, ProcessActivity} +// +//import java.io.FileOutputStream +//import java.time.Instant +// +//class PdfExporterSpec extends AnyFlatSpec with Matchers { +// +// private val history = List( +// ScenarioVersion(VersionId.initialVersionId, Instant.now(), "Zenon Wojciech", Option.empty, List.empty) +// ) +// +// it should "export process to " in { +// val scenarioGraph: ScenarioGraph = +// CanonicalProcessConverter.toScenarioGraph(ProcessTestData.sampleScenario) +// val graphWithFilterWithComment: ScenarioGraph = scenarioGraph.copy(nodes = scenarioGraph.nodes.map { +// case a: Filter => +// a.copy(additionalFields = Some(UserDefinedAdditionalNodeFields(Some("mój wnikliwy komętaż"), None))) +// case a => a +// }) +// +// val details = createDetails(graphWithFilterWithComment) +// +// val comments = (1 to 29) +// .map(commentId => +// Comment( +// commentId, +// details.processVersionId, +// "Jakiś taki dziwny ten proces??", +// "Wacław Wójcik", +// Instant.now() +// ) +// ) +// .toList +// +// val activities = ProcessActivity(comments, List()) +// +// val svg: String = ResourceLoader.load("/svg/svgTest.svg") +// val exported = PdfExporter.exportToPdf(svg, details, activities) +// +// IOUtils.write(exported, new FileOutputStream("/tmp/out.pdf")) +// } +// +// it should "export empty process to " in { +// val scenarioGraph: ScenarioGraph = ScenarioGraph( +// ProcessProperties(StreamMetaData()), +// List(), +// List(), +// ) +// +// val details = createDetails(scenarioGraph) +// +// val activities = ProcessActivity(List(), List()) +// val svg: String = ResourceLoader.load("/svg/svgTest.svg") +// val exported = PdfExporter.exportToPdf(svg, details, activities) +// +// IOUtils.write(exported, new FileOutputStream("/tmp/empty.pdf")) +// } +// +// it should "not allow entities in provided SVG" in { +// val scenarioGraph: ScenarioGraph = ScenarioGraph( +// ProcessProperties(StreamMetaData()), +// List(), +// List(), +// ) +// +// val details = createDetails(scenarioGraph) +// +// val activities = ProcessActivity(List(), List()) +// val svg: String = ResourceLoader.load("/svg/unsafe.svg") +// val ex = intercept[Exception] { +// PdfExporter.exportToPdf(svg, details, activities) +// } +// ex.getMessage should include("DOCTYPE is disallowed") +// } +// +// private def createDetails(scenarioGraph: ScenarioGraph) = TestProcessUtil.wrapWithScenarioDetailsEntity( +// ProcessName("My process"), +// scenarioGraph = Some(scenarioGraph), +// description = Some( +// "My fancy description, which is quite, quite, quite looooooooong. \n And it contains maaaany, maaany strange features..." +// ), +// history = Some(history) +// ) +// +//} diff --git a/docs-internal/api/nu-designer-openapi.yaml b/docs-internal/api/nu-designer-openapi.yaml index 5e78df23cba..a2087b57d32 100644 --- a/docs-internal/api/nu-designer-openapi.yaml +++ b/docs-internal/api/nu-designer-openapi.yaml @@ -2249,12 +2249,364 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/{versionId}/activity/comments: + /api/scenarioParametersCombinations: + get: + tags: + - App + summary: Service providing available combinations of scenario's parameters + operationId: getApiScenarioparameterscombinations + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScenarioParametersCombinationWithEngineErrors' + examples: + Example: + summary: List of available parameters combinations + value: + combinations: + - processingMode: Unbounded-Stream + category: Marketing + engineSetupName: Flink + - processingMode: Request-Response + category: Fraud + engineSetupName: Lite K8s + - processingMode: Unbounded-Stream + category: Fraud + engineSetupName: Flink Fraud Detection + engineSetupErrors: + Flink: + - Invalid Flink configuration + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No impersonated user's data found for provided identity + value: No impersonated user data found for provided identity + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/statistic: post: tags: - - Scenario - summary: Add scenario comment service - operationId: postApiProcessesScenarionameVersionidActivityComments + - Statistics + summary: Register statistics service + operationId: postApiStatistic + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterStatisticsRequestDto' + required: true + responses: + '204': + description: '' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: body' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No impersonated user's data found for provided identity + value: No impersonated user data found for provided identity + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/statistic/usage: + get: + tags: + - Statistics + summary: Statistics URL service + operationId: getApiStatisticUsage + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/StatisticUrlResponseDto' + examples: + Example: + summary: List of statistics URLs + value: + urls: + - https://stats.nussknacker.io/?a_n=1&a_t=0&a_v=0&c=3&c_n=82&c_t=0&c_v=0&f_m=0&f_v=0&fingerprint=development&n_m=2&n_ma=0&n_mi=2&n_v=1&s_a=0&s_dm_c=1&s_dm_e=1&s_dm_f=2&s_dm_l=0&s_f=1&s_pm_b=0&s_pm_rr=1&s_pm_s=3&s_s=3&source=sources&u_ma=0&u_mi=0&u_v=0&v_m=2&v_ma=1&v_mi=3&v_v=2&version=1.15.0-SNAPSHOT + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No impersonated user's data found for provided identity + value: No impersonated user data found for provided identity + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Statistics generation failed. + value: Statistics generation failed. + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/user: + get: + tags: + - User + summary: Logged user info service + operationId: getApiUser + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/DisplayableUser' + examples: + Example0: + summary: Common user info + value: + id: reader + username: reader + isAdmin: false + categories: + - Category1 + categoryPermissions: + Category1: + - Read + globalPermissions: [] + Example1: + summary: Admin user info + value: + id: admin + username: admin + isAdmin: true + categories: + - Category1 + - Category2 + categoryPermissions: {} + globalPermissions: [] + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No impersonated user's data found for provided identity + value: No impersonated user data found for provided identity + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/{versionId}/activity/attachments: + post: + tags: + - Activities + summary: Add scenario attachment service + operationId: postApiProcessesScenarionameVersionidActivityAttachments parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2274,18 +2626,25 @@ paths: schema: type: integer format: int64 + - name: Content-Disposition + in: header + required: true + schema: + type: string requestBody: content: - text/plain: + application/octet-stream: schema: type: string + format: binary required: true responses: '200': description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter versionId, Invalid value for: body' + value for: path parameter versionId, Invalid value for: body, Invalid + value for: header Content-Disposition' content: text/plain: schema: @@ -2336,12 +2695,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/comments/{commentId}: - delete: + /api/processes/{scenarioName}/{versionId}/activity/comments: + post: tags: - - Scenario - summary: Delete process comment service - operationId: deleteApiProcessesScenarionameActivityCommentsCommentid + - Activities + summary: Add scenario comment service + operationId: postApiProcessesScenarionameVersionidActivityComments parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2355,18 +2714,24 @@ paths: required: true schema: type: string - - name: commentId + - name: versionId in: path required: true schema: type: integer format: int64 + requestBody: + content: + text/plain: + schema: + type: string + required: true responses: '200': description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter commentId' + value for: path parameter versionId, Invalid value for: body' content: text/plain: schema: @@ -2403,16 +2768,6 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found - '500': - description: '' - content: - text/plain: - schema: - type: string - examples: - Example: - summary: 'Unable to delete comment with id: {commentId}' - value: 'Unable to delete comment with id: 1' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2427,12 +2782,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity: + /api/processes/{scenarioName}/activity/attachments: get: tags: - - Scenario - summary: Scenario activity service - operationId: getApiProcessesScenarionameActivity + - Activities + summary: Scenario attachments service + operationId: getApiProcessesScenarionameActivityAttachments parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2452,20 +2807,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ScenarioActivity' + $ref: '#/components/schemas/ScenarioAttachments' examples: Example: summary: Display scenario activity value: - comments: - - id: 1 - processVersionId: 1 - content: some comment - user: test - createDate: '2024-01-17T14:21:17Z' attachments: - id: 1 - processVersionId: 1 + scenarioVersion: 1 fileName: some_file.txt user: test createDate: '2024-01-17T14:21:17Z' @@ -2521,12 +2870,12 @@ paths: security: - {} - httpAuth: [] - /api/scenarioParametersCombinations: - get: + /api/processes/{scenarioName}/activity/comments/{scenarioActivityId}: + put: tags: - - App - summary: Service providing available combinations of scenario's parameters - operationId: getApiScenarioparameterscombinations + - Activities + summary: Edit process comment service + operationId: putApiProcessesScenarionameActivityCommentsScenarioactivityid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2535,32 +2884,29 @@ paths: type: - string - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: scenarioActivityId + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + text/plain: + schema: + type: string + required: true responses: '200': description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ScenarioParametersCombinationWithEngineErrors' - examples: - Example: - summary: List of available parameters combinations - value: - combinations: - - processingMode: Unbounded-Stream - category: Marketing - engineSetupName: Flink - - processingMode: Request-Response - category: Fraud - engineSetupName: Lite K8s - - processingMode: Unbounded-Stream - category: Fraud - engineSetupName: Flink Fraud Detection - engineSetupErrors: - Flink: - - Invalid Flink configuration '400': - description: 'Invalid value for: header Nu-Impersonate-User-Identity' + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter scenarioActivityId, Invalid value for: body' content: text/plain: schema: @@ -2595,8 +2941,18 @@ paths: type: string examples: Example: - summary: No impersonated user's data found for provided identity - value: No impersonated user data found for provided identity + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment 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: @@ -2611,12 +2967,11 @@ paths: security: - {} - httpAuth: [] - /api/statistic: - post: + delete: tags: - - Statistics - summary: Register statistics service - operationId: postApiStatistic + - Activities + summary: Delete process comment service + operationId: deleteApiProcessesScenarionameActivityCommentsScenarioactivityid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2625,18 +2980,23 @@ paths: type: - string - 'null' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/RegisterStatisticsRequestDto' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: scenarioActivityId + in: path required: true + schema: + type: string + format: uuid responses: - '204': + '200': description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: body' + value for: path parameter scenarioActivityId' content: text/plain: schema: @@ -2671,8 +3031,18 @@ paths: type: string examples: Example: - summary: No impersonated user's data found for provided identity - value: No impersonated user data found for provided identity + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment 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: @@ -2687,12 +3057,12 @@ paths: security: - {} - httpAuth: [] - /api/statistic/usage: + /api/processes/{scenarioName}/activity/attachments/{attachmentId}: get: tags: - - Statistics - summary: Statistics URL service - operationId: getApiStatisticUsage + - Activities + summary: Download attachment service + operationId: getApiProcessesScenarionameActivityAttachmentsAttachmentid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2701,21 +3071,39 @@ paths: type: - string - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: attachmentId + in: path + required: true + schema: + type: integer + format: int64 responses: '200': description: '' + headers: + Content-Disposition: + required: false + schema: + type: + - string + - 'null' + Content-Type: + required: true + schema: + type: string content: - application/json: + application/octet-stream: schema: - $ref: '#/components/schemas/StatisticUrlResponseDto' - examples: - Example: - summary: List of statistics URLs - value: - urls: - - https://stats.nussknacker.io/?a_n=1&a_t=0&a_v=0&c=3&c_n=82&c_t=0&c_v=0&f_m=0&f_v=0&fingerprint=development&n_m=2&n_ma=0&n_mi=2&n_v=1&s_a=0&s_dm_c=1&s_dm_e=1&s_dm_f=2&s_dm_l=0&s_f=1&s_pm_b=0&s_pm_rr=1&s_pm_s=3&s_s=3&source=sources&u_ma=0&u_mi=0&u_v=0&v_m=2&v_ma=1&v_mi=3&v_v=2&version=1.15.0-SNAPSHOT + type: string + format: binary '400': - description: 'Invalid value for: header Nu-Impersonate-User-Identity' + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter attachmentId' content: text/plain: schema: @@ -2750,18 +3138,8 @@ paths: type: string examples: Example: - summary: No impersonated user's data found for provided identity - value: No impersonated user data found for provided identity - '500': - description: '' - content: - text/plain: - schema: - type: string - examples: - Example: - summary: Statistics generation failed. - value: Statistics generation failed. + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2776,12 +3154,12 @@ paths: security: - {} - httpAuth: [] - /api/user: + /api/processes/{scenarioName}/activity: get: tags: - - User - summary: Logged user info service - operationId: getApiUser + - Activities + summary: Scenario activities service + operationId: getApiProcessesScenarionameActivity parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2790,37 +3168,172 @@ paths: type: - string - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string responses: '200': description: '' content: application/json: schema: - $ref: '#/components/schemas/DisplayableUser' + $ref: '#/components/schemas/ScenarioActivities' examples: - Example0: - summary: Common user info - value: - id: reader - username: reader - isAdmin: false - categories: - - Category1 - categoryPermissions: - Category1: - - Read - globalPermissions: [] - Example1: - summary: Admin user info + Example: + summary: Display scenario actions value: - id: admin - username: admin - isAdmin: true - categories: - - Category1 - - Category2 - categoryPermissions: {} - globalPermissions: [] + activities: + - id: 80c95497-3b53-4435-b2d9-ae73c5766213 + type: SCENARIO_CREATED + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + additionalFields: [] + - id: 070a4e5c-21e5-4e63-acac-0052cf705a90 + type: SCENARIO_ARCHIVED + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + additionalFields: [] + - id: fa35d944-fe20-4c4f-96c6-316b6197951a + type: SCENARIO_UNARCHIVED + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + additionalFields: [] + - id: 545b7d87-8cdf-4cb5-92c4-38ddbfca3d08 + type: SCENARIO_DEPLOYED + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: Deployment of scenario - task JIRA-1234 + additionalFields: [] + - id: c354eba1-de97-455c-b977-74729c41ce7 + type: SCENARIO_CANCELED + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: Canceled because marketing campaign ended + additionalFields: [] + - id: 07b04d45-c7c0-4980-a3bc-3c7f66410f68 + type: SCENARIO_MODIFIED + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: Added new processing step + additionalFields: [] + overrideDisplayableName: Version 1 saved + - id: da3d1f78-7d73-4ed9-b0e5-95538e150d0d + type: SCENARIO_NAME_CHANGED + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + additionalFields: + - name: oldName + value: marketing campaign + - name: newName + value: old marketing campaign + - id: edf8b047-9165-445d-a173-ba61812dbd63 + type: COMMENT_ADDED + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: Now scenario handles errors in datasource better + additionalFields: [] + - id: 369367d6-d445-4327-ac23-4a94367b1d9e + type: COMMENT_ADDED + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + additionalFields: + - name: deletedByUser + value: John Doe + overrideSupportedActions: [] + - id: b29916a9-34d4-4fc2-a6ab-79569f68c0b2 + type: ATTACHMENT_ADDED + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + additionalFields: + - name: attachmentId + value: '10000001' + - name: attachmentFilename + value: attachment01.png + - id: d0a7f4a2-abcc-4ffa-b1ca-68f6da3e999a + type: ATTACHMENT_ADDED + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + additionalFields: + - name: deletedByUser + value: John Doe + overrideSupportedActions: [] + - id: 683df470-0b33-4ead-bf61-fa35c63484f3 + type: CHANGED_PROCESSING_MODE + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + additionalFields: + - name: from + value: Request-Response + - name: to + value: Batch + - id: 4da0f1ac-034a-49b6-81c9-8ee48ba1d830 + type: INCOMING_MIGRATION + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: Migration from preprod + additionalFields: + - name: sourceEnvironment + value: preprod + - name: sourceScenarioVersion + value: '23' + - id: 49fcd45d-3fa6-48d4-b8ed-b3055910c7ad + type: OUTGOING_MIGRATION + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: Migration to preprod + additionalFields: + - name: destinationEnvironment + value: preprod + - id: 924dfcd3-fbc7-44ea-8763-813874382204 + type: PERFORMED_SINGLE_EXECUTION + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + additionalFields: + - name: dateFinished + value: '2024-01-17T14:21:17Z' + - name: status + value: Successfully executed + - id: 9b27797e-aa03-42ba-8406-d0ae8005a883 + type: PERFORMED_SCHEDULED_EXECUTION + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + additionalFields: + - name: params + value: Batch size=1 + - name: dateFinished + value: '2024-01-17T14:21:17Z' + - name: status + value: Successfully executed + - id: 33509d37-7657-4229-940f-b5736c82fb13 + type: AUTOMATIC_UPDATE + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + additionalFields: + - name: changes + value: JIRA-12345, JIRA-32146 + - name: dateFinished + value: '2024-01-17T14:21:17Z' + - name: status + value: Successful '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity' content: @@ -2857,8 +3370,8 @@ paths: type: string examples: Example: - summary: No impersonated user's data found for provided identity - value: No impersonated user data found for provided identity + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2875,6 +3388,17 @@ paths: - httpAuth: [] components: schemas: + AdditionalField: + title: AdditionalField + type: object + required: + - name + - value + properties: + name: + type: string + value: + type: string AdditionalInfo: title: AdditionalInfo oneOf: @@ -2893,7 +3417,7 @@ components: type: object required: - id - - processVersionId + - scenarioVersion - fileName - user - createDate @@ -2901,7 +3425,7 @@ components: id: type: integer format: int64 - processVersionId: + scenarioVersion: type: integer format: int64 fileName: @@ -3000,29 +3524,6 @@ components: type: string aType: type: string - Comment: - title: Comment - type: object - required: - - id - - processVersionId - - content - - user - - createDate - properties: - id: - type: integer - format: int64 - processVersionId: - type: integer - format: int64 - content: - type: string - user: - type: string - createDate: - type: string - format: date-time ComponentLink: title: ComponentLink type: object @@ -4415,14 +4916,75 @@ components: type: - string - 'null' + ScenarioActivities: + title: ScenarioActivities + type: object + properties: + activities: + type: array + items: + $ref: '#/components/schemas/ScenarioActivity' ScenarioActivity: title: ScenarioActivity type: object + required: + - id + - type + - user + - date properties: - comments: + id: + type: string + type: + type: string + enum: + - SCENARIO_CREATED + - SCENARIO_ARCHIVED + - SCENARIO_UNARCHIVED + - SCENARIO_DEPLOYED + - SCENARIO_CANCELED + - SCENARIO_MODIFIED + - SCENARIO_NAME_CHANGED + - COMMENT_ADDED + - ATTACHMENT_ADDED + - CHANGED_PROCESSING_MODE + - INCOMING_MIGRATION + - OUTGOING_MIGRATION + - PERFORMED_SINGLE_EXECUTION + - PERFORMED_SCHEDULED_EXECUTION + - AUTOMATIC_UPDATE + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + type: + - string + - 'null' + additionalFields: type: array items: - $ref: '#/components/schemas/Comment' + $ref: '#/components/schemas/AdditionalField' + overrideDisplayableName: + type: + - string + - 'null' + overrideSupportedActions: + type: + - array + - 'null' + items: + type: string + ScenarioAttachments: + title: ScenarioAttachments + type: object + properties: attachments: type: array items: diff --git a/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala new file mode 100644 index 00000000000..e1bad5fbab5 --- /dev/null +++ b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala @@ -0,0 +1,234 @@ +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 User( + id: 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, + ) extends ScenarioComment + + final case class Deleted( + deletedByUserName: UserName, + ) extends ScenarioComment + +} + +sealed trait ScenarioAttachment + +object ScenarioAttachment { + + final case class Available( + attachmentId: AttachmentId, + attachmentFilename: AttachmentFilename, + lastModifiedByUserName: UserName, + ) extends ScenarioAttachment + + final case class Deleted( + attachmentFilename: AttachmentFilename, + deletedByUserName: UserName + ) 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: User + def date: Instant + def scenarioVersion: Option[ScenarioVersion] +} + +object ScenarioActivity { + + final case class ScenarioCreated( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: User, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + ) extends ScenarioActivity + + final case class ScenarioArchived( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: User, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + ) extends ScenarioActivity + + final case class ScenarioUnarchived( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: User, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + ) extends ScenarioActivity + + // Scenario deployments + + final case class ScenarioDeployed( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: User, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + comment: ScenarioComment, + ) extends ScenarioActivity + + final case class ScenarioPaused( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: User, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + comment: ScenarioComment, + ) extends ScenarioActivity + + final case class ScenarioCanceled( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: User, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + comment: ScenarioComment, + ) extends ScenarioActivity + + // Scenario modifications + + final case class ScenarioModified( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: User, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + comment: ScenarioComment, + ) extends ScenarioActivity + + final case class ScenarioNameChanged( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: User, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + comment: ScenarioComment, + oldName: String, + newName: String, + ) extends ScenarioActivity + + final case class CommentAdded( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: User, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + comment: ScenarioComment, + ) extends ScenarioActivity + + final case class AttachmentAdded( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: User, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + attachment: ScenarioAttachment, + ) extends ScenarioActivity + + final case class ChangedProcessingMode( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: User, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + from: ProcessingMode, + to: ProcessingMode, + ) extends ScenarioActivity + + // Migration between environments + + final case class IncomingMigration( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: User, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + sourceEnvironment: Environment, + sourceScenarioVersion: ScenarioVersion, + ) extends ScenarioActivity + + final case class OutgoingMigration( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: User, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + comment: ScenarioComment, + destinationEnvironment: Environment, + ) extends ScenarioActivity + + // Batch + + final case class PerformedSingleExecution( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: User, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + dateFinished: Instant, + errorMessage: Option[String], + ) extends ScenarioActivity + + final case class PerformedScheduledExecution( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: User, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + dateFinished: Instant, + errorMessage: Option[String], + ) extends ScenarioActivity + + // Other/technical + + final case class AutomaticUpdate( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: User, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + dateFinished: Instant, + changes: String, + errorMessage: Option[String], + ) extends ScenarioActivity + +} From 3e928f98efecf57cc5843d906578f03c10603e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Fri, 6 Sep 2024 17:27:48 +0200 Subject: [PATCH 04/43] Restored legacy PDF exporter, legacy API endpoints --- .../ui/api/ProcessesExportResources.scala | 243 +++--- .../api/ScenarioActivityApiHttpService.scala | 39 +- .../description/scenarioActivity/Dtos.scala | 35 +- .../scenarioActivity/Endpoints.scala | 13 +- .../scenarioActivity/Examples.scala | 31 +- .../ScenarioActivityEntityFactory.scala | 29 +- .../repository/ProcessActionRepository.scala | 12 +- .../repository/ProcessRepository.scala | 2 - .../DbScenarioActivityRepository.scala | 262 ++++-- .../ScenarioActivityRepository.scala | 11 +- .../server/AkkaHttpBasedRouteProvider.scala | 14 +- .../nussknacker/ui/util/PdfExporter.scala | 783 +++++++++--------- .../ui/api/ManagementResourcesSpec.scala | 4 +- .../ProcessesExportImportResourcesSpec.scala | 348 ++++---- .../ScenarioAttachmentServiceSpec.scala | 7 +- .../deployment/DeploymentServiceSpec.scala | 3 +- .../DBFetchingProcessRepositorySpec.scala | 18 +- .../nussknacker/ui/util/PdfExporterSpec.scala | 195 +++-- .../api/deployment/ProcessActivity.scala | 10 +- 19 files changed, 1164 insertions(+), 895 deletions(-) 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 61376e8ace4..504db6e898e 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 @@ -1,118 +1,125 @@ -//package pl.touk.nussknacker.ui.api -// -//import akka.http.scaladsl.model._ -//import akka.http.scaladsl.server.{Directives, Route} -//import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} -//import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport -//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.process.ProcessService -//import pl.touk.nussknacker.ui.process.marshall.CanonicalProcessConverter -//import pl.touk.nussknacker.ui.process.processingtype.ProcessingTypeDataProvider -//import pl.touk.nussknacker.ui.process.repository.{ -// FetchingProcessRepository, -// ScenarioWithDetailsEntity -//} -//import pl.touk.nussknacker.ui.security.api.LoggedUser -//import pl.touk.nussknacker.ui.uiresolving.UIProcessResolver -//import pl.touk.nussknacker.ui.util._ -// -//import scala.concurrent.{ExecutionContext, Future} -// -//class ProcessesExportResources( -// processRepository: FetchingProcessRepository[Future], -// protected val processService: ProcessService, -// processActivityRepository: ProcessActivityRepository, -// processResolvers: ProcessingTypeDataProvider[UIProcessResolver, _] -//)(implicit val ec: ExecutionContext) -// extends Directives -// with FailFastCirceSupport -// with RouteWithUser -// with ProcessDirectives -// with NuPathMatchers { -// -// private implicit final val string: FromEntityUnmarshaller[String] = -// Unmarshaller.stringUnmarshaller.forContentTypes(ContentTypeRange.*) -// -// def securedRoute(implicit user: LoggedUser): Route = { -// path("processesExport" / ProcessNameSegment) { processName => -// (get & processId(processName)) { processId => -// complete { -// processRepository.fetchLatestProcessDetailsForProcessId[ScenarioGraph](processId.id).map { -// exportProcess -// } -// } -// } ~ (post & processDetailsForName(processName)) { processDetails => -// entity(as[ScenarioGraph]) { process => -// complete { -// exportResolvedProcess( -// process, -// processDetails.processingType, -// processDetails.name, -// processDetails.isFragment -// ) -// } -// } -// } -// } ~ path("processesExport" / ProcessNameSegment / VersionIdSegment) { (processName, versionId) => -// (get & processId(processName)) { processId => -// complete { -// processRepository.fetchProcessDetailsForId[ScenarioGraph](processId.id, versionId).map { -// exportProcess -// } -// } -// } -// } ~ path("processesExport" / "pdf" / ProcessNameSegment / VersionIdSegment) { (processName, versionId) => -// (post & processId(processName)) { processId => -// entity(as[String]) { svg => -// complete { -// processRepository.fetchProcessDetailsForId[ScenarioGraph](processId.id, versionId).flatMap { process => -// processActivityRepository.findActivity(processId.id).map(exportProcessToPdf(svg, process, _)) -// } -// } -// } -// } -// } -// } -// -// private def exportProcess(processDetails: Option[ScenarioWithDetailsEntity[ScenarioGraph]]): HttpResponse = -// processDetails.map(details => exportProcess(details.json, details.name)).getOrElse { -// HttpResponse(status = StatusCodes.NotFound, entity = "Scenario not found") -// } -// -// private def exportProcess(processDetails: ScenarioGraph, name: ProcessName): HttpResponse = { -// fileResponse(CanonicalProcessConverter.fromScenarioGraph(processDetails, name)) -// } -// -// private def exportResolvedProcess( -// processWithDictLabels: ScenarioGraph, -// processingType: ProcessingType, -// processName: ProcessName, -// isFragment: Boolean -// )(implicit user: LoggedUser): HttpResponse = { -// val processResolver = processResolvers.forProcessingTypeUnsafe(processingType) -// val resolvedProcess = processResolver.validateAndResolve(processWithDictLabels, processName, isFragment) -// fileResponse(resolvedProcess) -// } -// -// private def fileResponse(canonicalProcess: CanonicalProcess) = { -// val canonicalJson = canonicalProcess.asJson.spaces2 -// val entity = HttpEntity(ContentTypes.`application/json`, canonicalJson) -// AkkaHttpResponse.asFile(entity, s"${canonicalProcess.name}.json") -// } -// -// private def exportProcessToPdf( -// svg: String, -// processDetails: Option[ScenarioWithDetailsEntity[ScenarioGraph]], -// processActivity: ProcessActivity -// ) = processDetails match { -// case Some(process) => -// val pdf = PdfExporter.exportToPdf(svg, process, processActivity) -// HttpResponse(status = StatusCodes.OK, entity = HttpEntity(pdf)) -// case None => -// HttpResponse(status = StatusCodes.NotFound, entity = "Scenario not found") -// } -// -//} +package pl.touk.nussknacker.ui.api + +import akka.http.scaladsl.model._ +import akka.http.scaladsl.server.{Directives, Route} +import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} +import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport +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.description.scenarioActivity.Dtos.Legacy +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.Legacy.ProcessActivity +import pl.touk.nussknacker.ui.process.ProcessService +import pl.touk.nussknacker.ui.process.marshall.CanonicalProcessConverter +import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider +import pl.touk.nussknacker.ui.process.repository.activities.ScenarioActivityRepository +import pl.touk.nussknacker.ui.process.repository.{ + DBIOActionRunner, + FetchingProcessRepository, + ScenarioWithDetailsEntity +} +import pl.touk.nussknacker.ui.security.api.LoggedUser +import pl.touk.nussknacker.ui.uiresolving.UIProcessResolver +import pl.touk.nussknacker.ui.util._ + +import scala.concurrent.{ExecutionContext, Future} + +class ProcessesExportResources( + processRepository: FetchingProcessRepository[Future], + protected val processService: ProcessService, + scenarioActivityRepository: ScenarioActivityRepository, + processResolvers: ProcessingTypeDataProvider[UIProcessResolver, _], + dbioActionRunner: DBIOActionRunner, +)(implicit val ec: ExecutionContext) + extends Directives + with FailFastCirceSupport + with RouteWithUser + with ProcessDirectives + with NuPathMatchers { + + private implicit final val string: FromEntityUnmarshaller[String] = + Unmarshaller.stringUnmarshaller.forContentTypes(ContentTypeRange.*) + + def securedRoute(implicit user: LoggedUser): Route = { + path("processesExport" / ProcessNameSegment) { processName => + (get & processId(processName)) { processId => + complete { + processRepository.fetchLatestProcessDetailsForProcessId[ScenarioGraph](processId.id).map { + exportProcess + } + } + } ~ (post & processDetailsForName(processName)) { processDetails => + entity(as[ScenarioGraph]) { process => + complete { + exportResolvedProcess( + process, + processDetails.processingType, + processDetails.name, + processDetails.isFragment + ) + } + } + } + } ~ path("processesExport" / ProcessNameSegment / VersionIdSegment) { (processName, versionId) => + (get & processId(processName)) { processId => + complete { + processRepository.fetchProcessDetailsForId[ScenarioGraph](processId.id, versionId).map { + exportProcess + } + } + } + } ~ path("processesExport" / "pdf" / ProcessNameSegment / VersionIdSegment) { (processName, versionId) => + (post & processId(processName)) { processId => + entity(as[String]) { svg => + complete { + processRepository.fetchProcessDetailsForId[ScenarioGraph](processId.id, versionId).flatMap { process => + dbioActionRunner.run { + scenarioActivityRepository.findActivity(processId.id).map(exportProcessToPdf(svg, process, _)) + } + } + } + } + } + } + } + + private def exportProcess(processDetails: Option[ScenarioWithDetailsEntity[ScenarioGraph]]): HttpResponse = + processDetails.map(details => exportProcess(details.json, details.name)).getOrElse { + HttpResponse(status = StatusCodes.NotFound, entity = "Scenario not found") + } + + private def exportProcess(processDetails: ScenarioGraph, name: ProcessName): HttpResponse = { + fileResponse(CanonicalProcessConverter.fromScenarioGraph(processDetails, name)) + } + + private def exportResolvedProcess( + processWithDictLabels: ScenarioGraph, + processingType: ProcessingType, + processName: ProcessName, + isFragment: Boolean + )(implicit user: LoggedUser): HttpResponse = { + val processResolver = processResolvers.forProcessingTypeUnsafe(processingType) + val resolvedProcess = processResolver.validateAndResolve(processWithDictLabels, processName, isFragment) + fileResponse(resolvedProcess) + } + + private def fileResponse(canonicalProcess: CanonicalProcess) = { + val canonicalJson = canonicalProcess.asJson.spaces2 + val entity = HttpEntity(ContentTypes.`application/json`, canonicalJson) + AkkaHttpResponse.asFile(entity, s"${canonicalProcess.name}.json") + } + + private def exportProcessToPdf( + svg: String, + processDetails: Option[ScenarioWithDetailsEntity[ScenarioGraph]], + processActivity: ProcessActivity + ) = processDetails match { + case Some(process) => + val pdf = PdfExporter.exportToPdf(svg, process, processActivity) + HttpResponse(status = StatusCodes.OK, entity = HttpEntity(pdf)) + case None => + HttpResponse(status = StatusCodes.NotFound, entity = "Scenario not found") + } + +} 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 8257481d7ea..9d92aa23c74 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 @@ -47,6 +47,18 @@ class ScenarioActivityApiHttpService( private val endpoints = new Endpoints(securityInput, streamEndpointProvider) + expose { + endpoints.deprecatedScenarioActivityEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => scenarioName: ProcessName => + for { + scenarioId <- getScenarioIdByName(scenarioName) + _ <- isAuthorized(scenarioId, Permission.Read) + processActivity <- fetchProcessActivity(scenarioId) + } yield processActivity + } + } + expose { endpoints.scenarioActivitiesEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) @@ -139,13 +151,23 @@ class ScenarioActivityApiHttpService( } ) - private def fetchActivities(scenarioId: ProcessId)( - implicit loggedUser: LoggedUser + 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.fetchActivities(scenarioId) + scenarioActivityRepository.findActivities(scenarioId) ) ) .map(_.map(toDto).toList) @@ -241,13 +263,12 @@ class ScenarioActivityApiHttpService( scenarioVersion = scenarioVersion.map(_.value), comment = toDto(comment), ) - case ScenarioActivity.ScenarioNameChanged(_, id, user, date, version, comment, oldName, newName) => + 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), - comment = toDto(comment), oldName = oldName, newName = newName, ) @@ -363,6 +384,14 @@ class ScenarioActivityApiHttpService( changes = changes, errorMessage = errorMessage, ) + case ScenarioActivity.CustomAction(_, scenarioActivityId, user, date, scenarioVersion, actionName) => + Dtos.ScenarioActivity.CustomAction( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value), + actionName = actionName, + ) } } 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 ee8d70ca1c8..fdd80d453a5 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 @@ -8,7 +8,6 @@ import io.circe import io.circe.generic.extras.Configuration import io.circe.generic.extras.semiauto.deriveConfiguredCodec import io.circe.{Decoder, Encoder} -import pl.touk.nussknacker.engine.api.deployment.ScenarioVersion import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions import pl.touk.nussknacker.ui.api.BaseHttpService.CustomAuthorizationError @@ -242,7 +241,6 @@ object Dtos { user: String, date: Instant, scenarioVersion: Option[Long], - comment: ScenarioActivityComment, oldName: String, newName: String, ) extends ScenarioActivity @@ -324,6 +322,14 @@ object Dtos { errorMessage: Option[String], ) extends ScenarioActivity + final case class CustomAction( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + actionName: String, + ) extends ScenarioActivity + } // @@ -749,4 +755,29 @@ object Dtos { } + object Legacy { + + @derive(encoder, decoder, schema) + final case class ProcessActivity private (comments: List[Comment], attachments: List[Attachment]) + + @derive(encoder, decoder, schema) + final case class Comment( + id: Long, + processVersionId: Long, + content: String, + user: String, + createDate: Instant + ) + + @derive(encoder, decoder, schema) + final case class Attachment( + id: Long, + processVersionId: Long, + fileName: String, + user: String, + createDate: Instant + ) + + } + } 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 6f6a7835a8d..4f3328b4156 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 @@ -25,6 +25,17 @@ class Endpoints(auth: EndpointInput[AuthCredentials], streamProvider: TapirStrea import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos._ import pl.touk.nussknacker.ui.api.description.scenarioActivity.InputOutput._ + lazy val deprecatedScenarioActivityEndpoint + : SecuredEndpoint[ProcessName, ScenarioActivityError, Legacy.ProcessActivity, Any] = + baseNuApiEndpoint + .summary("Scenario activity service") + .tag("Scenario") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity") + .out(statusCode(Ok).and(jsonBody[Legacy.ProcessActivity].example(Examples.deprecatedScenarioActivity))) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + lazy val scenarioActivitiesEndpoint: SecuredEndpoint[ ProcessName, ScenarioActivityError, @@ -35,7 +46,7 @@ class Endpoints(auth: EndpointInput[AuthCredentials], streamProvider: TapirStrea .summary("Scenario activities service") .tag("Activities") .get - .in("processes" / path[ProcessName]("scenarioName") / "activity") + .in("processes" / path[ProcessName]("scenarioName") / "activities") .out(statusCode(Ok).and(jsonBody[ScenarioActivities].example(Examples.scenarioActivities))) .errorOut(scenarioNotFoundErrorOutput) .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 2812469e4eb..1050cd95c99 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,6 +1,6 @@ package pl.touk.nussknacker.ui.api.description.scenarioActivity -import pl.touk.nussknacker.engine.api.process.ProcessName +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._ import sttp.tapir.EndpointIO.Example @@ -10,6 +10,30 @@ import java.util.UUID object Examples { + val deprecatedScenarioActivity: Example[Legacy.ProcessActivity] = Example.of( + summary = Some("Display scenario activity"), + value = Legacy.ProcessActivity( + comments = List( + Legacy.Comment( + id = 1L, + processVersionId = 1L, + content = "some comment", + user = "test", + createDate = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + attachments = List( + Legacy.Attachment( + id = 1L, + processVersionId = 1L, + fileName = "some_file.txt", + user = "test", + createDate = Instant.parse("2024-01-17T14:21:17Z") + ) + ) + ) + ) + val scenarioActivities: Example[ScenarioActivities] = Example.of( summary = Some("Display scenario actions"), value = ScenarioActivities( @@ -70,11 +94,6 @@ object Examples { user = "some user", date = Instant.parse("2024-01-17T14:21:17Z"), scenarioVersion = Some(1), - comment = ScenarioActivityComment( - comment = Some("Changed name to better replect the purpose of this scenario"), - lastModifiedBy = "some user", - lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") - ), oldName = "marketing campaign", newName = "old marketing campaign", ), 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 index 98b8b4306eb..ae31a0aed99 100644 --- 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 @@ -13,6 +13,7 @@ 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 { @@ -87,7 +88,13 @@ trait ScenarioActivityEntityFactory extends BaseEntityFactory { } implicit def scenarioActivityTypeMapper: BaseColumnType[ScenarioActivityType] = - MappedColumnType.base[ScenarioActivityType, String](_.entryName, ScenarioActivityType.withName) + 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) @@ -131,6 +138,26 @@ object ScenarioActivityType extends Enum[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 } 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 index 4de36a5fadb..05724b84c04 100644 --- 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 @@ -6,6 +6,7 @@ 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} @@ -234,8 +235,10 @@ class DbProcessActionRepository( ScenarioActivityType.ScenarioPaused case ScenarioActionName.Rename => ScenarioActivityType.ScenarioNameChanged - case other => - throw new IllegalArgumentException(s"Action with id: $other can't be inserted") + case InstantBatchCustomAction.name => + ScenarioActivityType.PerformedSingleExecution + case otherCustomName => + ScenarioActivityType.CustomAction(otherCustomName.value) } val entity = ScenarioActivityEntityData( id = -1, @@ -402,7 +405,7 @@ class DbProcessActionRepository( ) } - private def toFinishedProcessAction(activityEntity: ScenarioActivityEntityData): ProcessAction = + private def toFinishedProcessAction(activityEntity: ScenarioActivityEntityData): ProcessAction = { ProcessAction( id = ProcessActionId(activityEntity.activityId.value), processId = ProcessId(activityEntity.scenarioId.value), @@ -424,6 +427,7 @@ class DbProcessActionRepository( comment = activityEntity.comment.map(_.value), buildInfo = activityEntity.buildInfo.flatMap(BuildInfo.parseJson).getOrElse(BuildInfo.empty) ) + } private def activityId(actionId: ProcessActionId) = ScenarioActivityId(actionId.value) @@ -462,6 +466,8 @@ class DbProcessActionRepository( None case ScenarioActivityType.AutomaticUpdate => None + case ScenarioActivityType.CustomAction(name) => + Some(ScenarioActionName(name)) } } 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 280acb47b0e..5e046d6348e 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 @@ -301,8 +301,6 @@ class DBProcessRepository( ), date = Instant.now(), scenarioVersion = Some(ScenarioVersion(version.id.value)), - // todo NU-1772 in progress - comment = ScenarioComment.Available("todomgw", UserName(loggedUser.username)), oldName = process.name.value, newName = newName.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 index ea2ec097839..55ae369d41a 100644 --- 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 @@ -8,6 +8,7 @@ import pl.touk.nussknacker.engine.api.deployment.ProcessActionState.ProcessActio 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, @@ -33,9 +34,15 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( import dbRef.profile.api._ - def fetchActivities( + def findActivities( scenarioId: ProcessId, - )(implicit user: LoggedUser): DB[Seq[ScenarioActivity]] = { + ): DB[Seq[ScenarioActivity]] = { + doFindActivities(scenarioId).map(_.map(_._2)) + } + + def doFindActivities( + scenarioId: ProcessId, + ): DB[Seq[(Long, ScenarioActivity)]] = { scenarioActivityTable .filter(_.scenarioId === scenarioId) .result @@ -134,6 +141,14 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( } yield activity.activityId } + def findAttachments( + scenarioId: ProcessId, + ): DB[Seq[AttachmentEntityData]] = { + attachmentsTable + .filter(_.processId === scenarioId) + .result + } + def findAttachment( scenarioId: ProcessId, attachmentId: Long, @@ -147,7 +162,88 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( def findActivity( processId: ProcessId - ): DB[String] = ??? + ): DB[Legacy.ProcessActivity] = { + for { + attachmentEntities <- findAttachments(processId) + attachments = attachmentEntities.map(toDto) + activities <- doFindActivities(processId) + comments = activities.flatMap(toComment) + } yield Legacy.ProcessActivity( + comments = comments.toList, + attachments = attachments.toList, + ) + } + + 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 val PrefixDeployedDeploymentComment = "Deployment: " + private val PrefixCanceledDeploymentComment = "Stop: " + private val PrefixRunNowDeploymentComment = "Run now: " + private val NoPrefix = "" + + private def toComment(idAndActivity: (Long, ScenarioActivity)): Option[Legacy.Comment] = { + val (id, scenarioActivity) = idAndActivity + scenarioActivity match { + case activity: ScenarioActivity.ScenarioCreated => + None + case activity: ScenarioActivity.ScenarioArchived => + None + case activity: 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("Cancel: ")) + 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("")), + None + ) + case activity: ScenarioActivity.CommentAdded => + toComment(id, activity, activity.comment, None) + case activity: ScenarioActivity.AttachmentAdded => + None + case activity: ScenarioActivity.ChangedProcessingMode => + None + case activity: ScenarioActivity.IncomingMigration => + None + case activity: ScenarioActivity.OutgoingMigration => + None + case activity: ScenarioActivity.PerformedSingleExecution => + None + case activity: ScenarioActivity.PerformedScheduledExecution => + None + case activity: ScenarioActivity.AutomaticUpdate => + None + case activity: ScenarioActivity.CustomAction => + None + } + } def getActivityStats: DB[Map[String, Int]] = ??? @@ -237,6 +333,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( 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, @@ -315,8 +412,6 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( ) case activity: ScenarioActivity.ScenarioNameChanged => createEntity(scenarioActivity)( - comment = comment(activity.comment), - lastModifiedByUserName = lastModifiedByUserName(activity.comment), additionalProperties = AdditionalProperties( Map( "oldName" -> activity.oldName, @@ -379,7 +474,6 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( createEntity(scenarioActivity)( finishedAt = Some(Timestamp.from(activity.dateFinished)), errorMessage = activity.errorMessage, - // todomgw execution params ) case activity: ScenarioActivity.AutomaticUpdate => createEntity(scenarioActivity)( @@ -391,6 +485,8 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( ) ) ) + case activity: ScenarioActivity.CustomAction => + createEntity(scenarioActivity)() } } @@ -445,7 +541,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( entity.additionalProperties.properties.get(name).toRight(s"Missing additional property $name") } - def fromEntity(entity: ScenarioActivityEntityData): Either[String, ScenarioActivity] = { + def fromEntity(entity: ScenarioActivityEntityData): Either[String, (Long, ScenarioActivity)] = { entity.activityType match { case ScenarioActivityType.ScenarioCreated => ScenarioActivity @@ -457,6 +553,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( scenarioVersion = entity.scenarioVersion ) .asRight + .map((entity.id, _)) case ScenarioActivityType.ScenarioArchived => ScenarioActivity .ScenarioArchived( @@ -467,6 +564,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( scenarioVersion = entity.scenarioVersion ) .asRight + .map((entity.id, _)) case ScenarioActivityType.ScenarioUnarchived => ScenarioActivity .ScenarioUnarchived( @@ -477,53 +575,61 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( 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, - ) - } + 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, - ) - } + 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, - ) - } + 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, - ) - } + 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 { - comment <- commentFromEntity(entity) + (for { oldName <- additionalPropertyFromEntity(entity, "oldName") newName <- additionalPropertyFromEntity(entity, "newName") } yield ScenarioActivity.ScenarioNameChanged( @@ -532,12 +638,11 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( user = userFromEntity(entity), date = entity.createdAt.toInstant, scenarioVersion = entity.scenarioVersion, - comment = comment, oldName = oldName, newName = newName - ) + )).map((entity.id, _)) case ScenarioActivityType.CommentAdded => - for { + (for { comment <- commentFromEntity(entity) } yield ScenarioActivity.CommentAdded( scenarioId = scenarioIdFromEntity(entity), @@ -546,9 +651,9 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( date = entity.createdAt.toInstant, scenarioVersion = entity.scenarioVersion, comment = comment, - ) + )).map((entity.id, _)) case ScenarioActivityType.AttachmentAdded => - for { + (for { attachment <- attachmentFromEntity(entity) } yield ScenarioActivity.AttachmentAdded( scenarioId = scenarioIdFromEntity(entity), @@ -557,9 +662,9 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( date = entity.createdAt.toInstant, scenarioVersion = entity.scenarioVersion, attachment = attachment, - ) + )).map((entity.id, _)) case ScenarioActivityType.ChangedProcessingMode => - for { + (for { from <- additionalPropertyFromEntity(entity, "fromProcessingMode").flatMap( ProcessingMode.withNameEither(_).left.map(_.getMessage()) ) @@ -574,9 +679,9 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( scenarioVersion = entity.scenarioVersion, from = from, to = to, - ) + )).map((entity.id, _)) case ScenarioActivityType.IncomingMigration => - for { + (for { sourceEnvironment <- additionalPropertyFromEntity(entity, "sourceEnvironment") sourceScenarioVersion <- additionalPropertyFromEntity(entity, "sourceScenarioVersion").flatMap( _.toLongOption.toRight("sourceScenarioVersion is not a valid Long") @@ -589,9 +694,9 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( scenarioVersion = entity.scenarioVersion, sourceEnvironment = Environment(sourceEnvironment), sourceScenarioVersion = ScenarioVersion(sourceScenarioVersion) - ) + )).map((entity.id, _)) case ScenarioActivityType.OutgoingMigration => - for { + (for { comment <- commentFromEntity(entity) destinationEnvironment <- additionalPropertyFromEntity(entity, "destinationEnvironment") } yield ScenarioActivity.OutgoingMigration( @@ -602,9 +707,9 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( scenarioVersion = entity.scenarioVersion, comment = comment, destinationEnvironment = Environment(destinationEnvironment), - ) + )).map((entity.id, _)) case ScenarioActivityType.PerformedSingleExecution => - for { + (for { finishedAt <- entity.finishedAt.map(_.toInstant).toRight("Missing finishedAt") } yield ScenarioActivity.PerformedSingleExecution( scenarioId = scenarioIdFromEntity(entity), @@ -614,9 +719,9 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( scenarioVersion = entity.scenarioVersion, dateFinished = finishedAt, errorMessage = entity.errorMessage, - ) + )).map((entity.id, _)) case ScenarioActivityType.PerformedScheduledExecution => - for { + (for { finishedAt <- entity.finishedAt.map(_.toInstant).toRight("Missing finishedAt") } yield ScenarioActivity.PerformedScheduledExecution( scenarioId = scenarioIdFromEntity(entity), @@ -626,9 +731,9 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( scenarioVersion = entity.scenarioVersion, dateFinished = finishedAt, errorMessage = entity.errorMessage, - ) + )).map((entity.id, _)) case ScenarioActivityType.AutomaticUpdate => - for { + (for { finishedAt <- entity.finishedAt.map(_.toInstant).toRight("Missing finishedAt") description <- additionalPropertyFromEntity(entity, "description") } yield ScenarioActivity.AutomaticUpdate( @@ -640,8 +745,31 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( dateFinished = finishedAt, errorMessage = entity.errorMessage, changes = description, - ) + )).map((entity.id, _)) + + case ScenarioActivityType.CustomAction(actionName) => + ScenarioActivity + .CustomAction( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + actionName = actionName, + ) + .asRight + .map((entity.id, _)) } } + 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, + ) + } + } 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 index 43ee59dfbb1..094f7edd233 100644 --- 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 @@ -3,6 +3,7 @@ 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 @@ -10,9 +11,9 @@ import pl.touk.nussknacker.ui.security.api.LoggedUser trait ScenarioActivityRepository { - def fetchActivities( + def findActivities( scenarioId: ProcessId, - )(implicit user: LoggedUser): DB[Seq[ScenarioActivity]] + ): DB[Seq[ScenarioActivity]] def addActivity( scenarioActivity: ScenarioActivity, @@ -37,6 +38,10 @@ trait ScenarioActivityRepository { attachmentToAdd: AttachmentToAdd )(implicit user: LoggedUser): DB[ScenarioActivityId] + def findAttachments( + scenarioId: ProcessId, + ): DB[Seq[AttachmentEntityData]] + def findAttachment( scenarioId: ProcessId, attachmentId: Long, @@ -44,7 +49,7 @@ trait ScenarioActivityRepository { def findActivity( processId: ProcessId - ): DB[String] + ): DB[Legacy.ProcessActivity] def getActivityStats: DB[Map[String, Int]] 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 332ed201804..792997276ed 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 @@ -418,13 +418,13 @@ class AkkaHttpBasedRouteProvider( processAuthorizer = processAuthorizer, processChangeListener = processChangeListener ), -// todo NU-1772 in progress -// new ProcessesExportResources( -// futureProcessRepository, -// processService, -// scenarioActivityRepository, -// processResolver -// ), + new ProcessesExportResources( + futureProcessRepository, + processService, + scenarioActivityRepository, + processResolver, + dbioRunner, + ), new ManagementResources( processAuthorizer, processService, 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 abf6b26e386..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 @@ -1,392 +1,391 @@ -// todo NU-1772 in progress -//package pl.touk.nussknacker.ui.util -// -//import java.io._ -//import java.net.URI -//import java.nio.charset.StandardCharsets -//import java.time.{Instant, ZoneId} -//import java.time.format.DateTimeFormatter -//import javax.xml.transform.TransformerFactory -//import javax.xml.transform.sax.SAXResult -//import javax.xml.transform.stream.StreamSource -//import com.typesafe.scalalogging.LazyLogging -//import org.apache.commons.io.IOUtils -//import org.apache.fop.apps.FopConfParser -//import org.apache.fop.apps.io.ResourceResolverFactory -//import org.apache.xmlgraphics.util.MimeConstants -//import pl.touk.nussknacker.engine.api.graph.ScenarioGraph -//import pl.touk.nussknacker.engine.graph.node._ -//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.process.repository.ScenarioWithDetailsEntity -// -//import scala.xml.{Elem, NodeSeq, XML} -// -//object PdfExporter extends LazyLogging { -// -// private val fopFactory = new FopConfParser( -// getClass.getResourceAsStream("/fop/config.xml"), -// new URI("http://touk.pl"), -// ResourceResolverFactory.createDefaultResourceResolver -// ).getFopFactoryBuilder.build -// -// def exportToPdf( -// svg: String, -// processDetails: ScenarioWithDetailsEntity[ScenarioGraph], -// processActivity: ProcessActivity -// ): Array[Byte] = { -// -// // initFontsIfNeeded is invoked every time to make sure that /tmp content is not deleted -// initFontsIfNeeded() -// // FIXME: cannot render polish signs..., better to strip them than not render anything... -// // \u00A0 - non-breaking space in not ASCII :)... -// val fopXml = prepareFopXml( -// svg.replaceAll("\u00A0", " ").replaceAll("[^\\p{ASCII}]", ""), -// processDetails, -// processActivity, -// processDetails.json -// ) -// -// createPdf(fopXml) -// } -// -// // in PDF export we print timezone, to avoid ambiguity -// // TODO: pass client timezone from FE -// private def format(instant: Instant) = { -// val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss [VV]") -// instant.atZone(ZoneId.systemDefault()).format(formatter) -// } -// -// // TODO: this is one nasty hack, is there a better way to make fop read fonts from classpath? -// private def initFontsIfNeeded(): Unit = synchronized { -// val dir = new File("/tmp/fop/fonts") -// dir.mkdirs() -// List( -// "OpenSans-BoldItalic.ttf", -// "OpenSans-Bold.ttf", -// "OpenSans-ExtraBoldItalic.ttf", -// "OpenSans-ExtraBold.ttf", -// "OpenSans-Italic.ttf", -// "OpenSans-LightItalic.ttf", -// "OpenSans-Light.ttf", -// "OpenSans-Regular.ttf", -// "OpenSans-SemiboldItalic.ttf", -// "OpenSans-Semibold.ttf" -// ).filterNot(name => new File(dir, name).exists()).foreach { name => -// IOUtils.copy(getClass.getResourceAsStream(s"/fop/fonts/$name"), new FileOutputStream(new File(dir, name))) -// } -// } -// -// private def createPdf(fopXml: Elem): Array[Byte] = { -// val out = new ByteArrayOutputStream() -// val fop = fopFactory.newFop(MimeConstants.MIME_PDF, out) -// val src = new StreamSource(new ByteArrayInputStream(fopXml.toString().getBytes(StandardCharsets.UTF_8))) -// TransformerFactory.newInstance().newTransformer().transform(src, new SAXResult(fop.getDefaultHandler)) -// out.toByteArray -// } -// -// private def prepareFopXml( -// svg: String, -// processDetails: ScenarioWithDetailsEntity[ScenarioGraph], -// processActivity: ProcessActivity, -// scenarioGraph: ScenarioGraph -// ) = { -// val diagram = XML.loadString(svg) -// val currentVersion = processDetails.history.get.find(_.processVersionId == processDetails.processVersionId).get -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// {processDetails.name} -// ( -// {processDetails.processCategory} -// ) -// -// -// -// Version: -// {processDetails.processVersionId} -// -// -// Saved by -// {currentVersion.user} -// at -// {format(currentVersion.createDate)} -// -// -// {processDetails.description.getOrElse("")} -// -// -// -// {diagram} -// -// -// {nodesSummary(scenarioGraph)} -// Nodes details -// {scenarioGraph.nodes.map(nodeDetails)}{comments(processActivity)}{attachments(processActivity)} -// -// -// -// -// } -// -// private def comments(processActivity: ProcessActivity) = -// -// -// Comments -// -// -// -// -// -// -// -// -// Date -// -// -// Author -// -// -// Comment -// -// -// -// -// { -// if (processActivity.comments.isEmpty) { -// -// -// -// } else -// processActivity.comments.sortBy(_.createDate).map { comment => -// -// -// -// {format(comment.createDate)} -// -// -// -// -// {comment.user} -// -// -// -// -// {comment.content} -// -// -// -// } -// } -// -//
-//
-//
-// -// private def nodeDetails(node: NodeData) = { -// val nodeData = node match { -// case Source(_, SourceRef(typ, params), _) => ("Type", typ) :: params.map(p => (p.name, p.expression.expression)) -// case Filter(_, expression, _, _) => List(("Expression", expression.expression)) -// case Enricher(_, ServiceRef(typ, params), output, _) => -// ("Type", typ) :: ("Output", output) :: params.map(p => (p.name, p.expression.expression)) -// // TODO: what about Swtich?? -// case Switch(_, expression, exprVal, _) => expression.map(e => ("Expression", e.expression)).toList -// case Processor(_, ServiceRef(typ, params), _, _) => -// ("Type", typ) :: params.map(p => (p.name, p.expression.expression)) -// case Sink(_, SinkRef(typ, params), _, _, _) => ("Type", typ) :: params.map(p => (p.name, p.expression.expression)) -// case CustomNode(_, output, typ, params, _) => -// ("Type", typ) :: ("Output", output.getOrElse("")) :: params.map(p => (p.name, p.expression.expression)) -// case FragmentInput(_, FragmentRef(typ, params, _), _, _, _) => -// ("Type", typ) :: params.map(p => (p.name, p.expression.expression)) -// case FragmentInputDefinition(_, parameters, _) => parameters.map(p => p.name -> p.typ.refClazzName) -// case FragmentOutputDefinition(_, outputName, fields, _) => -// ("Output name", outputName) :: fields.map(p => p.name -> p.expression.expression) -// case Variable(_, name, expr, _) => (name -> expr.expression) :: Nil -// case VariableBuilder(_, name, fields, _) => -// ("Variable name", name) :: fields.map(p => p.name -> p.expression.expression) -// case Join(_, output, typ, parameters, branch, _) => -// ("Type", typ) :: ("Output", output.getOrElse("")) :: -// parameters.map(p => p.name -> p.expression.expression) ++ branch.flatMap(bp => -// bp.parameters.map(p => s"${bp.branchId} - ${p.name}" -> p.expression.expression) -// ) -// case Split(_, _) => ("No parameters", "") :: Nil -// // This should not happen in properly resolved scenario... -// case _: BranchEndData => throw new IllegalArgumentException("Should not happen during PDF export") -// case _: FragmentUsageOutput => throw new IllegalArgumentException("Should not happen during PDF export") -// } -// val data = node.additionalFields -// .flatMap(_.description) -// .map(naf => ("Description", naf)) -// .toList ++ nodeData -// if (data.isEmpty) { -// NodeSeq.Empty -// } else { -// -// -// {node.getClass.getSimpleName}{node.id} -// -// -// -// -// -// { -// data.map { case (key, value) => -// -// -// -// {key} -// -// -// -// -// {addEmptySpace(value)} -// -// -// -// } -// } -// -//
-//
-// } -// -// } -// -// // we want to be able to break line for these characters. it's not really perfect solution for long, complex expressions, -// // but should handle most of the cases../ -// private def addEmptySpace(str: String) = List(")", ".", "(") -// .foldLeft(str) { (acc, el) => acc.replace(el, el + '\u200b') } -// -// private def nodesSummary(scenarioGraph: ScenarioGraph) = { -// -// -// Nodes summary -// -// -// -// -// -// -// -// -// Node name -// -// -// Type -// -// -// Description -// -// -// -// -// { -// if (scenarioGraph.nodes.isEmpty) { -// -// -// -// } else -// scenarioGraph.nodes.map { node => -// -// -// -// -// {node.id} -// -// -// -// -// -// {node.getClass.getSimpleName} -// -// -// -// -// {node.additionalFields.flatMap(_.description).getOrElse("")} -// -// -// -// } -// } -// -//
-//
-// } -// -// private def attachments(processActivity: ProcessActivity) = if (processActivity.attachments.isEmpty) { -// -// } else { -// -// -// Attachments -// -// -// -// -// -// -// -// -// -// -// Date -// -// -// Author -// -// -// File name -// -// -// -// -// { -// processActivity.attachments -// .sortBy(_.createDate) -// .map(attachment => -// -// -// -// {format(attachment.createDate)} -// -// -// -// -// {attachment.user} -// -// -// -// -// {attachment.fileName} -// -// -// ) -// } -// -//
-//
-// } -// -//} +package pl.touk.nussknacker.ui.util + +import java.io._ +import java.net.URI +import java.nio.charset.StandardCharsets +import java.time.{Instant, ZoneId} +import java.time.format.DateTimeFormatter +import javax.xml.transform.TransformerFactory +import javax.xml.transform.sax.SAXResult +import javax.xml.transform.stream.StreamSource +import com.typesafe.scalalogging.LazyLogging +import org.apache.commons.io.IOUtils +import org.apache.fop.apps.FopConfParser +import org.apache.fop.apps.io.ResourceResolverFactory +import org.apache.xmlgraphics.util.MimeConstants +import pl.touk.nussknacker.engine.api.graph.ScenarioGraph +import pl.touk.nussknacker.engine.graph.node._ +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.api.description.scenarioActivity.Dtos.Legacy.ProcessActivity +import pl.touk.nussknacker.ui.process.repository.ScenarioWithDetailsEntity + +import scala.xml.{Elem, NodeSeq, XML} + +object PdfExporter extends LazyLogging { + + private val fopFactory = new FopConfParser( + getClass.getResourceAsStream("/fop/config.xml"), + new URI("http://touk.pl"), + ResourceResolverFactory.createDefaultResourceResolver + ).getFopFactoryBuilder.build + + def exportToPdf( + svg: String, + processDetails: ScenarioWithDetailsEntity[ScenarioGraph], + processActivity: ProcessActivity + ): Array[Byte] = { + + // initFontsIfNeeded is invoked every time to make sure that /tmp content is not deleted + initFontsIfNeeded() + // FIXME: cannot render polish signs..., better to strip them than not render anything... + // \u00A0 - non-breaking space in not ASCII :)... + val fopXml = prepareFopXml( + svg.replaceAll("\u00A0", " ").replaceAll("[^\\p{ASCII}]", ""), + processDetails, + processActivity, + processDetails.json + ) + + createPdf(fopXml) + } + + // in PDF export we print timezone, to avoid ambiguity + // TODO: pass client timezone from FE + private def format(instant: Instant) = { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss [VV]") + instant.atZone(ZoneId.systemDefault()).format(formatter) + } + + // TODO: this is one nasty hack, is there a better way to make fop read fonts from classpath? + private def initFontsIfNeeded(): Unit = synchronized { + val dir = new File("/tmp/fop/fonts") + dir.mkdirs() + List( + "OpenSans-BoldItalic.ttf", + "OpenSans-Bold.ttf", + "OpenSans-ExtraBoldItalic.ttf", + "OpenSans-ExtraBold.ttf", + "OpenSans-Italic.ttf", + "OpenSans-LightItalic.ttf", + "OpenSans-Light.ttf", + "OpenSans-Regular.ttf", + "OpenSans-SemiboldItalic.ttf", + "OpenSans-Semibold.ttf" + ).filterNot(name => new File(dir, name).exists()).foreach { name => + IOUtils.copy(getClass.getResourceAsStream(s"/fop/fonts/$name"), new FileOutputStream(new File(dir, name))) + } + } + + private def createPdf(fopXml: Elem): Array[Byte] = { + val out = new ByteArrayOutputStream() + val fop = fopFactory.newFop(MimeConstants.MIME_PDF, out) + val src = new StreamSource(new ByteArrayInputStream(fopXml.toString().getBytes(StandardCharsets.UTF_8))) + TransformerFactory.newInstance().newTransformer().transform(src, new SAXResult(fop.getDefaultHandler)) + out.toByteArray + } + + private def prepareFopXml( + svg: String, + processDetails: ScenarioWithDetailsEntity[ScenarioGraph], + processActivity: ProcessActivity, + scenarioGraph: ScenarioGraph + ) = { + val diagram = XML.loadString(svg) + val currentVersion = processDetails.history.get.find(_.processVersionId == processDetails.processVersionId).get + + + + + + + + + + + + + + + + + + + + + + {processDetails.name} + ( + {processDetails.processCategory} + ) + + + + Version: + {processDetails.processVersionId} + + + Saved by + {currentVersion.user} + at + {format(currentVersion.createDate)} + + + {processDetails.description.getOrElse("")} + + + + {diagram} + + + {nodesSummary(scenarioGraph)} + Nodes details + {scenarioGraph.nodes.map(nodeDetails)}{comments(processActivity)}{attachments(processActivity)} + + + + + } + + private def comments(processActivity: ProcessActivity) = + + + Comments + + + + + + + + + Date + + + Author + + + Comment + + + + + { + if (processActivity.comments.isEmpty) { + + + + } else + processActivity.comments.sortBy(_.createDate).map { comment => + + + + {format(comment.createDate)} + + + + + {comment.user} + + + + + {comment.content} + + + + } + } + +
+
+
+ + private def nodeDetails(node: NodeData) = { + val nodeData = node match { + case Source(_, SourceRef(typ, params), _) => ("Type", typ) :: params.map(p => (p.name, p.expression.expression)) + case Filter(_, expression, _, _) => List(("Expression", expression.expression)) + case Enricher(_, ServiceRef(typ, params), output, _) => + ("Type", typ) :: ("Output", output) :: params.map(p => (p.name, p.expression.expression)) + // TODO: what about Swtich?? + case Switch(_, expression, exprVal, _) => expression.map(e => ("Expression", e.expression)).toList + case Processor(_, ServiceRef(typ, params), _, _) => + ("Type", typ) :: params.map(p => (p.name, p.expression.expression)) + case Sink(_, SinkRef(typ, params), _, _, _) => ("Type", typ) :: params.map(p => (p.name, p.expression.expression)) + case CustomNode(_, output, typ, params, _) => + ("Type", typ) :: ("Output", output.getOrElse("")) :: params.map(p => (p.name, p.expression.expression)) + case FragmentInput(_, FragmentRef(typ, params, _), _, _, _) => + ("Type", typ) :: params.map(p => (p.name, p.expression.expression)) + case FragmentInputDefinition(_, parameters, _) => parameters.map(p => p.name -> p.typ.refClazzName) + case FragmentOutputDefinition(_, outputName, fields, _) => + ("Output name", outputName) :: fields.map(p => p.name -> p.expression.expression) + case Variable(_, name, expr, _) => (name -> expr.expression) :: Nil + case VariableBuilder(_, name, fields, _) => + ("Variable name", name) :: fields.map(p => p.name -> p.expression.expression) + case Join(_, output, typ, parameters, branch, _) => + ("Type", typ) :: ("Output", output.getOrElse("")) :: + parameters.map(p => p.name -> p.expression.expression) ++ branch.flatMap(bp => + bp.parameters.map(p => s"${bp.branchId} - ${p.name}" -> p.expression.expression) + ) + case Split(_, _) => ("No parameters", "") :: Nil + // This should not happen in properly resolved scenario... + case _: BranchEndData => throw new IllegalArgumentException("Should not happen during PDF export") + case _: FragmentUsageOutput => throw new IllegalArgumentException("Should not happen during PDF export") + } + val data = node.additionalFields + .flatMap(_.description) + .map(naf => ("Description", naf)) + .toList ++ nodeData + if (data.isEmpty) { + NodeSeq.Empty + } else { + + + {node.getClass.getSimpleName}{node.id} + + + + + + { + data.map { case (key, value) => + + + + {key} + + + + + {addEmptySpace(value)} + + + + } + } + +
+
+ } + + } + + // we want to be able to break line for these characters. it's not really perfect solution for long, complex expressions, + // but should handle most of the cases../ + private def addEmptySpace(str: String) = List(")", ".", "(") + .foldLeft(str) { (acc, el) => acc.replace(el, el + '\u200b') } + + private def nodesSummary(scenarioGraph: ScenarioGraph) = { + + + Nodes summary + + + + + + + + + Node name + + + Type + + + Description + + + + + { + if (scenarioGraph.nodes.isEmpty) { + + + + } else + scenarioGraph.nodes.map { node => + + + + + {node.id} + + + + + + {node.getClass.getSimpleName} + + + + + {node.additionalFields.flatMap(_.description).getOrElse("")} + + + + } + } + +
+
+ } + + private def attachments(processActivity: ProcessActivity) = if (processActivity.attachments.isEmpty) { + + } else { + + + Attachments + + + + + + + + + + + Date + + + Author + + + File name + + + + + { + processActivity.attachments + .sortBy(_.createDate) + .map(attachment => + + + + {format(attachment.createDate)} + + + + + {attachment.user} + + + + + {attachment.fileName} + + + ) + } + +
+
+ } + +} 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 4fa6b55cdea..8a103509266 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 @@ -145,8 +145,8 @@ class ManagementResourcesSpec val expectedDeployComment = "Deployment: deployComment" val expectedStopComment = "Stop: cancelComment" getActivity(ProcessTestData.sampleScenario.name) ~> check { - val comments = responseAs[Dtos.ScenarioActivities].activities - // todomgw comments.map(_.content) shouldBe List(expectedDeployComment, expectedStopComment) + val comments = responseAs[Dtos.Legacy.ProcessActivity].comments.sortBy(_.id) + comments.map(_.content) shouldBe List(expectedDeployComment, expectedStopComment) val firstCommentId :: secondCommentId :: Nil = comments.map(_.id) 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 0c22d82adf2..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 @@ -1,174 +1,174 @@ -// todo NU-1772 in progress -//package pl.touk.nussknacker.ui.api -// -//import akka.http.scaladsl.model.{ContentTypeRange, ContentTypes, HttpEntity, StatusCodes} -//import akka.http.scaladsl.server.Route -//import akka.http.scaladsl.testkit.ScalatestRouteTest -//import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} -//import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport -//import io.circe.syntax._ -//import org.scalatest.funsuite.AnyFunSuite -//import org.scalatest.matchers.should.Matchers -//import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Inside, OptionValues} -//import pl.touk.nussknacker.engine.api.graph.ScenarioGraph -//import pl.touk.nussknacker.engine.api.{ProcessAdditionalFields, StreamMetaData} -//import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -//import pl.touk.nussknacker.engine.marshall.ProcessMarshaller -//import pl.touk.nussknacker.restmodel.validation.ScenarioGraphWithValidationResult -//import pl.touk.nussknacker.test.PatientScalaFutures -//import pl.touk.nussknacker.test.utils.domain.TestFactory.{asAdmin, processResolverByProcessingType, withAllPermissions} -//import pl.touk.nussknacker.test.base.it.NuResourcesTest -//import pl.touk.nussknacker.test.utils.domain.ProcessTestData -//import pl.touk.nussknacker.ui.process.marshall.CanonicalProcessConverter -//import pl.touk.nussknacker.ui.util.MultipartUtils -// -//class ProcessesExportImportResourcesSpec -// extends AnyFunSuite -// with ScalatestRouteTest -// with Matchers -// with Inside -// with FailFastCirceSupport -// with PatientScalaFutures -// with OptionValues -// with BeforeAndAfterEach -// with BeforeAndAfterAll -// with NuResourcesTest { -// -// import akka.http.scaladsl.server.RouteConcatenation._ -// -// private implicit final val string: FromEntityUnmarshaller[String] = -// Unmarshaller.stringUnmarshaller.forContentTypes(ContentTypeRange.*) -// -// private val processesExportResources = new ProcessesExportResources( -// futureFetchingScenarioRepository, -// processService, -// processActivityRepository, -// processResolverByProcessingType -// ) -// -// private val routeWithAllPermissions = withAllPermissions(processesExportResources) ~ -// withAllPermissions(processesRoute) -// private val adminRoute = asAdmin(processesExportResources) ~ asAdmin(processesRoute) -// -// test("export process from scenarioGraph") { -// val scenarioGraphToExport = ProcessTestData.sampleScenarioGraph -// createEmptyProcess(ProcessTestData.sampleProcessName) -// -// Post( -// s"/processesExport/${ProcessTestData.sampleProcessName}", -// scenarioGraphToExport -// ) ~> routeWithAllPermissions ~> check { -// status shouldEqual StatusCodes.OK -// val exported = responseAs[String] -// val processDetails = ProcessMarshaller.fromJson(exported).toOption.get -// -// processDetails shouldBe CanonicalProcessConverter.fromScenarioGraph( -// scenarioGraphToExport, -// ProcessTestData.sampleProcessName -// ) -// } -// } -// -// test("export process and import it (as common user)") { -// runImportExportTest(routeWithAllPermissions) -// } -// -// test("export process and import it (as admin)") { -// runImportExportTest(adminRoute) -// } -// -// private def runImportExportTest(route: Route): Unit = { -// val scenarioGraphToSave = ProcessTestData.sampleScenarioGraph -// saveProcess(scenarioGraphToSave) { -// status shouldEqual StatusCodes.OK -// } -// -// Get(s"/processesExport/${ProcessTestData.sampleProcessName}/2") ~> route ~> check { -// val response = responseAs[String] -// val processDetails = ProcessMarshaller.fromJson(response).toOption.get -// assertProcessPrettyPrinted(response, processDetails) -// -// val modified = processDetails.copy(metaData = -// processDetails.metaData.withTypeSpecificData(typeSpecificData = StreamMetaData(Some(987))) -// ) -// val multipartForm = MultipartUtils.prepareMultiPart(modified.asJson.spaces2, "process") -// Post(s"/processes/import/${ProcessTestData.sampleProcessName}", multipartForm) ~> route ~> check { -// status shouldEqual StatusCodes.OK -// val imported = responseAs[ScenarioGraphWithValidationResult] -// imported.scenarioGraph.properties.typeSpecificProperties.asInstanceOf[StreamMetaData].parallelism shouldBe Some( -// 987 -// ) -// imported.scenarioGraph.nodes shouldBe scenarioGraphToSave.nodes -// } -// } -// } -// -// test("export process in new version") { -// val description = "alamakota" -// val scenarioGraphToSave = ProcessTestData.sampleScenarioGraph -// val processWithDescription = scenarioGraphToSave.copy(properties = -// scenarioGraphToSave.properties.copy(additionalFields = -// ProcessAdditionalFields(Some(description), Map.empty, StreamMetaData.typeName) -// ) -// ) -// -// saveProcess(scenarioGraphToSave) { -// status shouldEqual StatusCodes.OK -// } -// updateProcess(processWithDescription) { -// status shouldEqual StatusCodes.OK -// } -// -// Get(s"/processesExport/${ProcessTestData.sampleProcessName}/2") ~> routeWithAllPermissions ~> check { -// val response = responseAs[String] -// response shouldNot include(description) -// assertProcessPrettyPrinted(response, scenarioGraphToSave) -// } -// -// Get(s"/processesExport/${ProcessTestData.sampleProcessName}/3") ~> routeWithAllPermissions ~> check { -// val latestProcessVersion = io.circe.parser.parse(responseAs[String]) -// latestProcessVersion.toOption.get.spaces2 should include(description) -// -// Get(s"/processesExport/${ProcessTestData.sampleProcessName}") ~> routeWithAllPermissions ~> check { -// io.circe.parser.parse(responseAs[String]) shouldBe latestProcessVersion -// } -// -// } -// -// } -// -// test("export pdf") { -// val scenarioGraphToSave = ProcessTestData.sampleScenarioGraph -// saveProcess(scenarioGraphToSave) { -// status shouldEqual StatusCodes.OK -// -// val testSvg = "\n " + -// "\n " + -// "\n" -// -// Post( -// s"/processesExport/pdf/${ProcessTestData.sampleProcessName}/2", -// HttpEntity(testSvg) -// ) ~> routeWithAllPermissions ~> check { -// -// status shouldEqual StatusCodes.OK -// contentType shouldEqual ContentTypes.`application/octet-stream` -// // just simple sanity check that it's really pdf... -// responseAs[String] should startWith("%PDF") -// } -// } -// -// } -// -// private def assertProcessPrettyPrinted(response: String, expectedProcess: CanonicalProcess): Unit = { -// response shouldBe expectedProcess.asJson.spaces2 -// } -// -// private def assertProcessPrettyPrinted(response: String, process: ScenarioGraph): Unit = { -// assertProcessPrettyPrinted( -// response, -// CanonicalProcessConverter.fromScenarioGraph(process, ProcessTestData.sampleProcessName) -// ) -// } -// -//} +package pl.touk.nussknacker.ui.api + +import akka.http.scaladsl.model.{ContentTypeRange, ContentTypes, HttpEntity, StatusCodes} +import akka.http.scaladsl.server.Route +import akka.http.scaladsl.testkit.ScalatestRouteTest +import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} +import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport +import io.circe.syntax._ +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers +import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Inside, OptionValues} +import pl.touk.nussknacker.engine.api.graph.ScenarioGraph +import pl.touk.nussknacker.engine.api.{ProcessAdditionalFields, StreamMetaData} +import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess +import pl.touk.nussknacker.engine.marshall.ProcessMarshaller +import pl.touk.nussknacker.restmodel.validation.ScenarioGraphWithValidationResult +import pl.touk.nussknacker.test.PatientScalaFutures +import pl.touk.nussknacker.test.utils.domain.TestFactory.{asAdmin, processResolverByProcessingType, withAllPermissions} +import pl.touk.nussknacker.test.base.it.NuResourcesTest +import pl.touk.nussknacker.test.utils.domain.ProcessTestData +import pl.touk.nussknacker.ui.process.marshall.CanonicalProcessConverter +import pl.touk.nussknacker.ui.util.MultipartUtils + +class ProcessesExportImportResourcesSpec + extends AnyFunSuite + with ScalatestRouteTest + with Matchers + with Inside + with FailFastCirceSupport + with PatientScalaFutures + with OptionValues + with BeforeAndAfterEach + with BeforeAndAfterAll + with NuResourcesTest { + + import akka.http.scaladsl.server.RouteConcatenation._ + + private implicit final val string: FromEntityUnmarshaller[String] = + Unmarshaller.stringUnmarshaller.forContentTypes(ContentTypeRange.*) + + private val processesExportResources = new ProcessesExportResources( + futureFetchingScenarioRepository, + processService, + scenarioActivityRepository, + processResolverByProcessingType, + dbioRunner, + ) + + private val routeWithAllPermissions = withAllPermissions(processesExportResources) ~ + withAllPermissions(processesRoute) + private val adminRoute = asAdmin(processesExportResources) ~ asAdmin(processesRoute) + + test("export process from scenarioGraph") { + val scenarioGraphToExport = ProcessTestData.sampleScenarioGraph + createEmptyProcess(ProcessTestData.sampleProcessName) + + Post( + s"/processesExport/${ProcessTestData.sampleProcessName}", + scenarioGraphToExport + ) ~> routeWithAllPermissions ~> check { + status shouldEqual StatusCodes.OK + val exported = responseAs[String] + val processDetails = ProcessMarshaller.fromJson(exported).toOption.get + + processDetails shouldBe CanonicalProcessConverter.fromScenarioGraph( + scenarioGraphToExport, + ProcessTestData.sampleProcessName + ) + } + } + + test("export process and import it (as common user)") { + runImportExportTest(routeWithAllPermissions) + } + + test("export process and import it (as admin)") { + runImportExportTest(adminRoute) + } + + private def runImportExportTest(route: Route): Unit = { + val scenarioGraphToSave = ProcessTestData.sampleScenarioGraph + saveProcess(scenarioGraphToSave) { + status shouldEqual StatusCodes.OK + } + + Get(s"/processesExport/${ProcessTestData.sampleProcessName}/2") ~> route ~> check { + val response = responseAs[String] + val processDetails = ProcessMarshaller.fromJson(response).toOption.get + assertProcessPrettyPrinted(response, processDetails) + + val modified = processDetails.copy(metaData = + processDetails.metaData.withTypeSpecificData(typeSpecificData = StreamMetaData(Some(987))) + ) + val multipartForm = MultipartUtils.prepareMultiPart(modified.asJson.spaces2, "process") + Post(s"/processes/import/${ProcessTestData.sampleProcessName}", multipartForm) ~> route ~> check { + status shouldEqual StatusCodes.OK + val imported = responseAs[ScenarioGraphWithValidationResult] + imported.scenarioGraph.properties.typeSpecificProperties.asInstanceOf[StreamMetaData].parallelism shouldBe Some( + 987 + ) + imported.scenarioGraph.nodes shouldBe scenarioGraphToSave.nodes + } + } + } + + test("export process in new version") { + val description = "alamakota" + val scenarioGraphToSave = ProcessTestData.sampleScenarioGraph + val processWithDescription = scenarioGraphToSave.copy(properties = + scenarioGraphToSave.properties.copy(additionalFields = + ProcessAdditionalFields(Some(description), Map.empty, StreamMetaData.typeName) + ) + ) + + saveProcess(scenarioGraphToSave) { + status shouldEqual StatusCodes.OK + } + updateProcess(processWithDescription) { + status shouldEqual StatusCodes.OK + } + + Get(s"/processesExport/${ProcessTestData.sampleProcessName}/2") ~> routeWithAllPermissions ~> check { + val response = responseAs[String] + response shouldNot include(description) + assertProcessPrettyPrinted(response, scenarioGraphToSave) + } + + Get(s"/processesExport/${ProcessTestData.sampleProcessName}/3") ~> routeWithAllPermissions ~> check { + val latestProcessVersion = io.circe.parser.parse(responseAs[String]) + latestProcessVersion.toOption.get.spaces2 should include(description) + + Get(s"/processesExport/${ProcessTestData.sampleProcessName}") ~> routeWithAllPermissions ~> check { + io.circe.parser.parse(responseAs[String]) shouldBe latestProcessVersion + } + + } + + } + + test("export pdf") { + val scenarioGraphToSave = ProcessTestData.sampleScenarioGraph + saveProcess(scenarioGraphToSave) { + status shouldEqual StatusCodes.OK + + val testSvg = "\n " + + "\n " + + "\n" + + Post( + s"/processesExport/pdf/${ProcessTestData.sampleProcessName}/2", + HttpEntity(testSvg) + ) ~> routeWithAllPermissions ~> check { + + status shouldEqual StatusCodes.OK + contentType shouldEqual ContentTypes.`application/octet-stream` + // just simple sanity check that it's really pdf... + responseAs[String] should startWith("%PDF") + } + } + + } + + private def assertProcessPrettyPrinted(response: String, expectedProcess: CanonicalProcess): Unit = { + response shouldBe expectedProcess.asJson.spaces2 + } + + private def assertProcessPrettyPrinted(response: String, process: ScenarioGraph): Unit = { + assertProcessPrettyPrinted( + response, + CanonicalProcessConverter.fromScenarioGraph(process, ProcessTestData.sampleProcessName) + ) + } + +} 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 14d9c029d30..33e54a2ead9 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 @@ -7,6 +7,7 @@ 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.process.repository.activities.ScenarioActivityRepository @@ -51,7 +52,7 @@ class ScenarioAttachmentServiceSpec extends AnyFunSuite with Matchers with Scala private object TestProcessActivityRepository extends ScenarioActivityRepository { - override def fetchActivities(scenarioId: ProcessId)(implicit user: LoggedUser): DB[Seq[ScenarioActivity]] = ??? + override def findActivities(scenarioId: ProcessId): DB[Seq[ScenarioActivity]] = ??? override def addActivity(scenarioActivity: ScenarioActivity)(implicit user: LoggedUser): DB[ScenarioActivityId] = ??? @@ -72,9 +73,11 @@ private object TestProcessActivityRepository extends ScenarioActivityRepository ): 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[String] = ??? + override def findActivity(processId: ProcessId): DB[ProcessActivity] = ??? override def getActivityStats: DB[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 6a104740bcb..60b6e0aceda 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 @@ -330,8 +330,7 @@ class DeploymentServiceSpec lastStateAction.state shouldBe ProcessActionState.ExecutionFinished // we want to hide finished deploys processDetails.lastDeployedAction shouldBe empty - // todo NU-1772 in progress - // dbioRunner.run(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 = 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 8059c6e894a..9bd1b58d4e0 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 @@ -14,6 +14,7 @@ import pl.touk.nussknacker.test.PatientScalaFutures import pl.touk.nussknacker.test.base.db.WithHsqlDbTesting 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.ProcessDBQueryRepository.ProcessAlreadyExists @@ -147,15 +148,14 @@ class DBFetchingProcessRepositorySpec renameProcess(oldName, newName) -// todo NU-1772 in progress -// val comments = fetching -// .fetchProcessId(newName) -// .flatMap(v => activities.findActivity(v.get).map(_.comments)) -// .futureValue -// -// atLeast(1, comments) should matchPattern { -// case Comment(_, VersionId(1L), "Rename: [oldName] -> [newName]", user.username, _) => -// } + val comments = fetching + .fetchProcessId(newName) + .flatMap(v => dbioRunner.run(activities.findActivity(v.get).map(_.comments))) + .futureValue + + atLeast(1, comments) should matchPattern { + case Comment(_, 1L, "Rename: [oldName] -> [newName]", user.username, _) => + } } test("should prevent rename to existing name") { 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 df49b47f848..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 @@ -1,98 +1,97 @@ -//todo NU-1772 in progress -//package pl.touk.nussknacker.ui.util -// -//import org.apache.commons.io.IOUtils -//import org.scalatest.flatspec.AnyFlatSpec -//import org.scalatest.matchers.should.Matchers -//import pl.touk.nussknacker.engine.api.StreamMetaData -//import pl.touk.nussknacker.engine.api.graph.{ProcessProperties, ScenarioGraph} -//import pl.touk.nussknacker.engine.api.process.{ProcessName, ScenarioVersion, VersionId} -//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.process.marshall.CanonicalProcessConverter -//import pl.touk.nussknacker.ui.process.repository.DbProcessActivityRepository.{Comment, ProcessActivity} -// -//import java.io.FileOutputStream -//import java.time.Instant -// -//class PdfExporterSpec extends AnyFlatSpec with Matchers { -// -// private val history = List( -// ScenarioVersion(VersionId.initialVersionId, Instant.now(), "Zenon Wojciech", Option.empty, List.empty) -// ) -// -// it should "export process to " in { -// val scenarioGraph: ScenarioGraph = -// CanonicalProcessConverter.toScenarioGraph(ProcessTestData.sampleScenario) -// val graphWithFilterWithComment: ScenarioGraph = scenarioGraph.copy(nodes = scenarioGraph.nodes.map { -// case a: Filter => -// a.copy(additionalFields = Some(UserDefinedAdditionalNodeFields(Some("mój wnikliwy komętaż"), None))) -// case a => a -// }) -// -// val details = createDetails(graphWithFilterWithComment) -// -// val comments = (1 to 29) -// .map(commentId => -// Comment( -// commentId, -// details.processVersionId, -// "Jakiś taki dziwny ten proces??", -// "Wacław Wójcik", -// Instant.now() -// ) -// ) -// .toList -// -// val activities = ProcessActivity(comments, List()) -// -// val svg: String = ResourceLoader.load("/svg/svgTest.svg") -// val exported = PdfExporter.exportToPdf(svg, details, activities) -// -// IOUtils.write(exported, new FileOutputStream("/tmp/out.pdf")) -// } -// -// it should "export empty process to " in { -// val scenarioGraph: ScenarioGraph = ScenarioGraph( -// ProcessProperties(StreamMetaData()), -// List(), -// List(), -// ) -// -// val details = createDetails(scenarioGraph) -// -// val activities = ProcessActivity(List(), List()) -// val svg: String = ResourceLoader.load("/svg/svgTest.svg") -// val exported = PdfExporter.exportToPdf(svg, details, activities) -// -// IOUtils.write(exported, new FileOutputStream("/tmp/empty.pdf")) -// } -// -// it should "not allow entities in provided SVG" in { -// val scenarioGraph: ScenarioGraph = ScenarioGraph( -// ProcessProperties(StreamMetaData()), -// List(), -// List(), -// ) -// -// val details = createDetails(scenarioGraph) -// -// val activities = ProcessActivity(List(), List()) -// val svg: String = ResourceLoader.load("/svg/unsafe.svg") -// val ex = intercept[Exception] { -// PdfExporter.exportToPdf(svg, details, activities) -// } -// ex.getMessage should include("DOCTYPE is disallowed") -// } -// -// private def createDetails(scenarioGraph: ScenarioGraph) = TestProcessUtil.wrapWithScenarioDetailsEntity( -// ProcessName("My process"), -// scenarioGraph = Some(scenarioGraph), -// description = Some( -// "My fancy description, which is quite, quite, quite looooooooong. \n And it contains maaaany, maaany strange features..." -// ), -// history = Some(history) -// ) -// -//} +package pl.touk.nussknacker.ui.util + +import org.apache.commons.io.IOUtils +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import pl.touk.nussknacker.engine.api.StreamMetaData +import pl.touk.nussknacker.engine.api.graph.{ProcessProperties, ScenarioGraph} +import pl.touk.nussknacker.engine.api.process.{ProcessName, ScenarioVersion, VersionId} +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 java.io.FileOutputStream +import java.time.Instant + +class PdfExporterSpec extends AnyFlatSpec with Matchers { + + private val history = List( + ScenarioVersion(VersionId.initialVersionId, Instant.now(), "Zenon Wojciech", Option.empty, List.empty) + ) + + it should "export process to " in { + val scenarioGraph: ScenarioGraph = + CanonicalProcessConverter.toScenarioGraph(ProcessTestData.sampleScenario) + val graphWithFilterWithComment: ScenarioGraph = scenarioGraph.copy(nodes = scenarioGraph.nodes.map { + case a: Filter => + a.copy(additionalFields = Some(UserDefinedAdditionalNodeFields(Some("mój wnikliwy komętaż"), None))) + case a => a + }) + + val details = createDetails(graphWithFilterWithComment) + + val comments = (1 to 29) + .map(commentId => + Comment( + commentId, + details.processVersionId.value, + "Jakiś taki dziwny ten proces??", + "Wacław Wójcik", + Instant.now() + ) + ) + .toList + + val activities = ProcessActivity(comments, List()) + + val svg: String = ResourceLoader.load("/svg/svgTest.svg") + val exported = PdfExporter.exportToPdf(svg, details, activities) + + IOUtils.write(exported, new FileOutputStream("/tmp/out.pdf")) + } + + it should "export empty process to " in { + val scenarioGraph: ScenarioGraph = ScenarioGraph( + ProcessProperties(StreamMetaData()), + List(), + List(), + ) + + val details = createDetails(scenarioGraph) + + val activities = ProcessActivity(List(), List()) + val svg: String = ResourceLoader.load("/svg/svgTest.svg") + val exported = PdfExporter.exportToPdf(svg, details, activities) + + IOUtils.write(exported, new FileOutputStream("/tmp/empty.pdf")) + } + + it should "not allow entities in provided SVG" in { + val scenarioGraph: ScenarioGraph = ScenarioGraph( + ProcessProperties(StreamMetaData()), + List(), + List(), + ) + + val details = createDetails(scenarioGraph) + + val activities = ProcessActivity(List(), List()) + val svg: String = ResourceLoader.load("/svg/unsafe.svg") + val ex = intercept[Exception] { + PdfExporter.exportToPdf(svg, details, activities) + } + ex.getMessage should include("DOCTYPE is disallowed") + } + + private def createDetails(scenarioGraph: ScenarioGraph) = TestProcessUtil.wrapWithScenarioDetailsEntity( + ProcessName("My process"), + scenarioGraph = Some(scenarioGraph), + description = Some( + "My fancy description, which is quite, quite, quite looooooooong. \n And it contains maaaany, maaany strange features..." + ), + history = Some(history) + ) + +} diff --git a/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala index e1bad5fbab5..72725ee5bbe 100644 --- a/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala +++ b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala @@ -141,7 +141,6 @@ object ScenarioActivity { user: User, date: Instant, scenarioVersion: Option[ScenarioVersion], - comment: ScenarioComment, oldName: String, newName: String, ) extends ScenarioActivity @@ -231,4 +230,13 @@ object ScenarioActivity { errorMessage: Option[String], ) extends ScenarioActivity + final case class CustomAction( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: User, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + actionName: String, + ) extends ScenarioActivity + } From be9cb20ce1a7b3bbbf0859bd3d773beebac4a6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 9 Sep 2024 01:31:54 +0200 Subject: [PATCH 05/43] Added proper comment handling --- .../api/ScenarioActivityApiHttpService.scala | 113 +- .../description/scenarioActivity/Dtos.scala | 473 ++--- .../scenarioActivity/Endpoints.scala | 76 +- .../repository/ProcessActionRepository.scala | 26 +- .../DbScenarioActivityRepository.scala | 145 +- .../ScenarioActivityRepository.scala | 13 + .../test/utils/OpenAPIExamplesValidator.scala | 8 +- .../ui/api/ManagementResourcesSpec.scala | 12 +- .../ScenarioAttachmentServiceSpec.scala | 24 +- docs-internal/api/nu-designer-openapi.yaml | 1589 ++++++++++++++--- 10 files changed, 1811 insertions(+), 668 deletions(-) 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 9d92aa23c74..daaf3ed7582 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 @@ -59,18 +59,66 @@ class ScenarioActivityApiHttpService( } } + expose { + endpoints.deprecatedAddCommentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: AddCommentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Write) + _ <- addNewComment(request, scenarioId) + } yield () + } + } + + expose { + endpoints.deprecatedEditCommentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: DeprecatedEditCommentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Write) + _ <- editComment(request, scenarioId) + } yield () + } + } + + expose { + endpoints.deprecatedDeleteCommentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: DeprecatedDeleteCommentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Write) + _ <- deleteComment(request, scenarioId) + } yield () + } + } + expose { endpoints.scenarioActivitiesEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => scenarioName: ProcessName => for { scenarioId <- getScenarioIdByName(scenarioName) - _ <- isAuthorized(scenarioId, Permission.Write) + _ <- isAuthorized(scenarioId, Permission.Read) activities <- fetchActivities(scenarioId) } yield ScenarioActivities(activities) } } + expose { + endpoints.scenarioActivitiesMetadataEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => scenarioName: ProcessName => + for { + scenarioId <- getScenarioIdByName(scenarioName) + _ <- isAuthorized(scenarioId, Permission.Read) + metadata = ScenarioActivitiesMetadata.default + } yield metadata + } + } + expose { endpoints.addCommentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) @@ -90,7 +138,7 @@ class ScenarioActivityApiHttpService( for { scenarioId <- getScenarioIdByName(request.scenarioName) _ <- isAuthorized(scenarioId, Permission.Write) - _ <- editComment(request) + _ <- editComment(request, scenarioId) } yield () } } @@ -102,11 +150,23 @@ class ScenarioActivityApiHttpService( for { scenarioId <- getScenarioIdByName(request.scenarioName) _ <- isAuthorized(scenarioId, Permission.Write) - _ <- deleteComment(request) + _ <- deleteComment(request, scenarioId) } yield () } } + expose { + endpoints.attachmentsEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => processName: ProcessName => + for { + scenarioId <- getScenarioIdByName(processName) + _ <- isAuthorized(scenarioId, Permission.Read) + attachments <- fetchAttachments(scenarioId) + } yield attachments + } + } + expose { endpoints.addAttachmentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) @@ -404,22 +464,61 @@ class ScenarioActivityApiHttpService( ) ) - private def editComment(request: EditCommentRequest)( + private def editComment(request: DeprecatedEditCommentRequest, scenarioId: ProcessId)( implicit loggedUser: LoggedUser ): EitherT[Future, ScenarioActivityError, Unit] = EitherT( dbioActionRunner.run( - scenarioActivityRepository.editComment(ScenarioActivityId(request.scenarioActivityId), request.commentContent) + scenarioActivityRepository.editComment(scenarioId, request.commentId, request.commentContent) + ) + ).leftMap(_ => NoComment(request.commentId.toString)) + + 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(_ => NoComment(request.scenarioActivityId.toString)) - private def deleteComment(request: DeleteCommentRequest)( + private def deleteComment(request: DeprecatedDeleteCommentRequest, scenarioId: ProcessId)( + implicit loggedUser: LoggedUser + ): EitherT[Future, ScenarioActivityError, Unit] = + EitherT( + dbioActionRunner.run(scenarioActivityRepository.deleteComment(scenarioId, request.commentId)) + ).leftMap(_ => NoComment(request.commentId.toString)) + + private def deleteComment(request: DeleteCommentRequest, scenarioId: ProcessId)( implicit loggedUser: LoggedUser ): EitherT[Future, ScenarioActivityError, Unit] = EitherT( - dbioActionRunner.run(scenarioActivityRepository.deleteComment(ScenarioActivityId(request.scenarioActivityId))) + dbioActionRunner.run( + scenarioActivityRepository.deleteComment(scenarioId, ScenarioActivityId(request.scenarioActivityId)) + ) ).leftMap(_ => NoComment(request.scenarioActivityId.toString)) + 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 ): EitherT[Future, ScenarioActivityError, Unit] = { 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 fdd80d453a5..5b9c30491e3 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 @@ -5,7 +5,7 @@ import derevo.derive import enumeratum.EnumEntry.UpperSnakecase import enumeratum.{Enum, EnumEntry} import io.circe -import io.circe.generic.extras.Configuration +import io.circe.generic.extras import io.circe.generic.extras.semiauto.deriveConfiguredCodec import io.circe.{Decoder, Encoder} import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} @@ -16,6 +16,7 @@ import pl.touk.nussknacker.ui.server.HeadersSupport.FileName import sttp.model.MediaType import sttp.tapir._ import sttp.tapir.derevo.schema +import sttp.tapir.generic.Configuration import java.io.InputStream import java.time.Instant @@ -24,6 +25,74 @@ import scala.collection.immutable object Dtos { + @derive(encoder, decoder, schema) + final case class ScenarioActivitiesMetadata( + activities: List[ScenarioActivityMetadata], + actions: List[ScenarioActivityActionMetadata], + ) + + object ScenarioActivitiesMetadata { + + val default: ScenarioActivitiesMetadata = ScenarioActivitiesMetadata( + activities = ScenarioActivityType.values.map(ScenarioActivityMetadata.from).toList, + actions = List( + ScenarioActivityActionMetadata( + id = "compare", + displayableName = "Compare", + icon = "/assets/states/error.svg" + ), + ScenarioActivityActionMetadata( + id = "delete_comment", + displayableName = "Delete", + icon = "/assets/states/error.svg" + ), + ScenarioActivityActionMetadata( + id = "edit_comment", + displayableName = "Edit", + icon = "/assets/states/error.svg" + ), + ScenarioActivityActionMetadata( + id = "download_attachment", + displayableName = "Download", + icon = "/assets/states/error.svg" + ), + ScenarioActivityActionMetadata( + id = "delete_attachment", + displayableName = "Delete", + icon = "/assets/states/error.svg" + ), + ) + ) + + } + + @derive(encoder, decoder, schema) + final case class ScenarioActivityActionMetadata( + id: String, + displayableName: String, + icon: String, + ) + + @derive(encoder, decoder, schema) + final case class ScenarioActivityMetadata( + `type`: String, + displayableName: String, + icon: String, + supportedActions: List[String], + ) + + object ScenarioActivityMetadata { + + def from(scenarioActivityType: ScenarioActivityType): ScenarioActivityMetadata = + ScenarioActivityMetadata( + `type` = scenarioActivityType.entryName, + displayableName = scenarioActivityType.displayableName, + icon = scenarioActivityType.icon, + supportedActions = scenarioActivityType.supportedActions, + ) + + } + sealed trait ScenarioActivityType extends EnumEntry with UpperSnakecase { def displayableName: String def icon: String @@ -56,6 +125,12 @@ object Dtos { override def supportedActions: List[String] = List.empty } + case object ScenarioPaused extends ScenarioActivityType { + override def displayableName: String = "Pause" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + case object ScenarioCanceled extends ScenarioActivityType { override def displayableName: String = "Cancel" override def icon: String = "/assets/states/error.svg" @@ -122,6 +197,12 @@ object Dtos { override def supportedActions: List[String] = List("compare") } + case object CustomAction extends ScenarioActivityType { + override def displayableName: String = "Custom action" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + override def values: immutable.IndexedSeq[ScenarioActivityType] = findValues implicit def scenarioActivityTypeSchema: Schema[ScenarioActivityType] = @@ -163,7 +244,6 @@ object Dtos { @derive(encoder, decoder, schema) final case class ScenarioActivities(activities: List[ScenarioActivity]) - @derive(schema) sealed trait ScenarioActivity { def id: UUID def user: String @@ -173,12 +253,18 @@ object Dtos { object ScenarioActivity { - implicit val scenarioActivityCodec: circe.Codec[ScenarioActivity] = { - implicit val configuration: Configuration = - Configuration.default.withDiscriminator("type").withScreamingSnakeCaseConstructorNames + implicit def scenarioActivityCodec: circe.Codec[ScenarioActivity] = { + implicit val configuration: extras.Configuration = + extras.Configuration.default.withDiscriminator("type").withScreamingSnakeCaseConstructorNames deriveConfiguredCodec } + implicit def scenarioActivitySchema: Schema[ScenarioActivity] = { + implicit val configuration: Configuration = + Configuration.default.withDiscriminator("type").withScreamingSnakeCaseDiscriminatorValues + Schema.derived[ScenarioActivity] + } + final case class ScenarioCreated( id: UUID, user: String, @@ -332,365 +418,6 @@ object Dtos { } -// -// @derive(schema) -// final case class ScenarioActivity( -// id: ScenarioActivityId, -// `type`: ScenarioActivityType, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// additionalFields: List[AdditionalField], -// overrideDisplayableName: Option[String] = None, -// overrideSupportedActions: Option[List[String]] = None -// ) -// -// object ScenarioActivity { -// - -// -// @derive(encoder, decoder, schema) -// final case class AdditionalField( -// name: String, -// value: String -// ) -// -// def forScenarioCreated( -// id: ScenarioActivityId, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// ): ScenarioActivity = ScenarioActivity( -// id = id, -// `type` = ScenarioActivityType.ScenarioCreated, -// user = user, -// date = date, -// scenarioVersion = scenarioVersion, -// comment = comment, -// additionalFields = List.empty, -// ) -// -// def forScenarioArchived( -// id: ScenarioActivityId, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// ): ScenarioActivity = ScenarioActivity( -// id = id, -// `type` = ScenarioActivityType.ScenarioArchived, -// user = user, -// date = date, -// scenarioVersion = scenarioVersion, -// comment = comment, -// additionalFields = List.empty, -// ) -// -// def forScenarioUnarchived( -// id: ScenarioActivityId, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// ): ScenarioActivity = ScenarioActivity( -// id = id, -// `type` = ScenarioActivityType.ScenarioUnarchived, -// user = user, -// date = date, -// scenarioVersion = scenarioVersion, -// comment = comment, -// additionalFields = List.empty, -// ) -// -// // Scenario deployments -// -// def forScenarioDeployed( -// id: ScenarioActivityId, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// ): ScenarioActivity = ScenarioActivity( -// id = id, -// `type` = ScenarioActivityType.ScenarioDeployed, -// user = user, -// date = date, -// scenarioVersion = scenarioVersion, -// comment = comment, -// additionalFields = List.empty, -// ) -// -// def forScenarioCanceled( -// id: ScenarioActivityId, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// ): ScenarioActivity = ScenarioActivity( -// id = id, -// `type` = ScenarioActivityType.ScenarioCanceled, -// user = user, -// date = date, -// scenarioVersion = scenarioVersion, -// comment = comment, -// additionalFields = List.empty, -// ) -// -// // Scenario modifications -// -// def forScenarioModified( -// id: ScenarioActivityId, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// ): ScenarioActivity = ScenarioActivity( -// id = id, -// `type` = ScenarioActivityType.ScenarioModified, -// user = user, -// date = date, -// scenarioVersion = scenarioVersion, -// comment = comment, -// additionalFields = List.empty, -// overrideDisplayableName = Some(s"Version $scenarioVersion saved"), -// ) -// -// def forScenarioNameChanged( -// id: ScenarioActivityId, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// oldName: String, -// newName: String, -// ): ScenarioActivity = ScenarioActivity( -// id = id, -// `type` = ScenarioActivityType.ScenarioNameChanged, -// user = user, -// date = date, -// scenarioVersion = scenarioVersion, -// comment = comment, -// additionalFields = List( -// AdditionalField("oldName", oldName), -// AdditionalField("newName", newName), -// ) -// ) -// -// def forCommentAdded( -// id: ScenarioActivityId, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// ): ScenarioActivity = ScenarioActivity( -// id = id, -// `type` = ScenarioActivityType.CommentAdded, -// user = user, -// date = date, -// scenarioVersion = scenarioVersion, -// comment = comment, -// additionalFields = List.empty, -// ) -// -// def forCommentAddedAndDeleted( -// id: ScenarioActivityId, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// deletedByUser: String, -// ): ScenarioActivity = ScenarioActivity( -// id = id, -// `type` = ScenarioActivityType.CommentAdded, -// user = user, -// date = date, -// scenarioVersion = scenarioVersion, -// comment = comment, -// additionalFields = List( -// AdditionalField("deletedByUser", deletedByUser), -// ), -// overrideSupportedActions = Some(List.empty) -// ) -// -// def forAttachmentPresent( -// id: ScenarioActivityId, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// attachmentId: String, -// attachmentFilename: String, -// ): ScenarioActivity = ScenarioActivity( -// id = id, -// `type` = ScenarioActivityType.AttachmentAdded, -// user = user, -// date = date, -// scenarioVersion = scenarioVersion, -// comment = comment, -// additionalFields = List( -// AdditionalField("attachmentId", attachmentId), -// AdditionalField("attachmentFilename", attachmentFilename), -// ) -// ) -// -// def forAttachmentDeleted( -// id: ScenarioActivityId, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// deletedByUser: String, -// ): ScenarioActivity = ScenarioActivity( -// id = id, -// `type` = ScenarioActivityType.AttachmentAdded, -// user = user, -// date = date, -// scenarioVersion = scenarioVersion, -// comment = comment, -// additionalFields = List( -// AdditionalField("deletedByUser", deletedByUser), -// ), -// overrideSupportedActions = Some(List.empty) -// ) -// -// def forChangedProcessingMode( -// id: ScenarioActivityId, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// from: String, -// to: String, -// ): ScenarioActivity = ScenarioActivity( -// id = id, -// `type` = ScenarioActivityType.ChangedProcessingMode, -// user = user, -// date = date, -// scenarioVersion = scenarioVersion, -// comment = comment, -// additionalFields = List( -// AdditionalField("from", from), -// AdditionalField("to", to), -// ) -// ) -// -// // Migration between environments -// -// def forIncomingMigration( -// id: ScenarioActivityId, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// sourceEnvironment: String, -// sourceScenarioVersion: String, -// ): ScenarioActivity = ScenarioActivity( -// id = id, -// `type` = ScenarioActivityType.IncomingMigration, -// user = user, -// date = date, -// scenarioVersion = scenarioVersion, -// comment = comment, -// additionalFields = List( -// AdditionalField("sourceEnvironment", sourceEnvironment), -// AdditionalField("sourceScenarioVersion", sourceScenarioVersion), -// ) -// ) -// -// def forOutgoingMigration( -// id: ScenarioActivityId, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// destinationEnvironment: String, -// ): ScenarioActivity = ScenarioActivity( -// id = id, -// `type` = ScenarioActivityType.OutgoingMigration, -// user = user, -// date = date, -// scenarioVersion = scenarioVersion, -// comment = comment, -// additionalFields = List( -// AdditionalField("destinationEnvironment", destinationEnvironment), -// ) -// ) -// -// // Batch -// -// def forPerformedSingleExecution( -// id: ScenarioActivityId, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// dateFinished: String, -// status: String, -// ): ScenarioActivity = ScenarioActivity( -// id = id, -// `type` = ScenarioActivityType.PerformedSingleExecution, -// user = user, -// date = date, -// scenarioVersion = scenarioVersion, -// comment = comment, -// additionalFields = List( -// AdditionalField("dateFinished", dateFinished), -// AdditionalField("status", status), -// ) -// ) -// -// def forPerformedScheduledExecution( -// id: ScenarioActivityId, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// dateFinished: String, -// params: String, -// status: String, -// ): ScenarioActivity = ScenarioActivity( -// id = id, -// `type` = ScenarioActivityType.PerformedScheduledExecution, -// user = user, -// date = date, -// scenarioVersion = scenarioVersion, -// comment = comment, -// additionalFields = List( -// AdditionalField("params", params), -// AdditionalField("dateFinished", dateFinished), -// AdditionalField("status", status), -// ) -// ) -// -// // Other/technical -// -// def forAutomaticUpdate( -// id: ScenarioActivityId, -// user: String, -// date: Instant, -// scenarioVersion: Option[ScenarioVersion], -// comment: Option[String], -// dateFinished: String, -// changes: String, -// status: String, -// ): ScenarioActivity = ScenarioActivity( -// id = id, -// `type` = ScenarioActivityType.AutomaticUpdate, -// user = user, -// date = date, -// scenarioVersion = scenarioVersion, -// comment = comment, -// additionalFields = List( -// AdditionalField("changes", changes), -// AdditionalField("dateFinished", dateFinished), -// AdditionalField("status", status), -// ) -// ) -// -// } - @derive(encoder, decoder, schema) final case class ScenarioAttachments(attachments: List[Attachment]) @@ -714,13 +441,27 @@ object Dtos { final case class AddCommentRequest(scenarioName: ProcessName, versionId: VersionId, commentContent: String) + final case class DeprecatedEditCommentRequest( + scenarioName: ProcessName, + commentId: Long, + commentContent: String + ) + final case class EditCommentRequest( scenarioName: ProcessName, scenarioActivityId: UUID, commentContent: String ) - final case class DeleteCommentRequest(scenarioName: ProcessName, scenarioActivityId: UUID) + final case class DeleteCommentRequest( + scenarioName: ProcessName, + scenarioActivityId: UUID + ) + + final case class DeprecatedDeleteCommentRequest( + scenarioName: ProcessName, + commentId: Long, + ) final case class AddAttachmentRequest( scenarioName: ProcessName, @@ -750,7 +491,7 @@ object Dtos { implicit val noCommentCodec: Codec[String, NoComment, CodecFormat.TextPlain] = BaseEndpointDefinitions.toTextPlainCodecSerializationOnly[NoComment](e => - s"Unable to delete comment for activity with id: ${e.scenarioActivityId}" + s"Unable to delete comment with id: ${e.scenarioActivityId}" ) } 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 4f3328b4156..ea9b24efb3b 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 @@ -29,12 +29,67 @@ class Endpoints(auth: EndpointInput[AuthCredentials], streamProvider: TapirStrea : SecuredEndpoint[ProcessName, ScenarioActivityError, Legacy.ProcessActivity, Any] = baseNuApiEndpoint .summary("Scenario activity service") - .tag("Scenario") + .tag("Activities") .get .in("processes" / path[ProcessName]("scenarioName") / "activity") .out(statusCode(Ok).and(jsonBody[Legacy.ProcessActivity].example(Examples.deprecatedScenarioActivity))) .errorOut(scenarioNotFoundErrorOutput) .withSecurity(auth) + .deprecated() + + lazy val deprecatedAddCommentEndpoint: SecuredEndpoint[AddCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Add scenario comment service") + .tag("Activities") + .post + .in("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" / "comments") + .in(stringBody) + .mapInTo[AddCommentRequest] + .out(statusCode(Ok)) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + .deprecated() + + lazy val deprecatedEditCommentEndpoint + : SecuredEndpoint[DeprecatedEditCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Edit process comment service") + .tag("Activities") + .put + .in( + "processes" / path[ProcessName]("scenarioName") / "activity" / "comments" / path[Long]("commentId") + ) + .in(stringBody) + .mapInTo[DeprecatedEditCommentRequest] + .out(statusCode(Ok)) + .errorOut( + oneOf[ScenarioActivityError]( + oneOfVariantFromMatchType(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), + oneOfVariantFromMatchType(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)) + ) + ) + .withSecurity(auth) + .deprecated() + + lazy val deprecatedDeleteCommentEndpoint + : SecuredEndpoint[DeprecatedDeleteCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Delete process comment service") + .tag("Activities") + .delete + .in( + "processes" / path[ProcessName]("scenarioName") / "activity" / "comments" / path[Long]("commentId") + ) + .mapInTo[DeprecatedDeleteCommentRequest] + .out(statusCode(Ok)) + .errorOut( + oneOf[ScenarioActivityError]( + oneOfVariantFromMatchType(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), + oneOfVariantFromMatchType(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)) + ) + ) + .withSecurity(auth) + .deprecated() lazy val scenarioActivitiesEndpoint: SecuredEndpoint[ ProcessName, @@ -46,17 +101,28 @@ class Endpoints(auth: EndpointInput[AuthCredentials], streamProvider: TapirStrea .summary("Scenario activities service") .tag("Activities") .get - .in("processes" / path[ProcessName]("scenarioName") / "activities") + .in("processes" / path[ProcessName]("scenarioName") / "activity" / "activities") .out(statusCode(Ok).and(jsonBody[ScenarioActivities].example(Examples.scenarioActivities))) .errorOut(scenarioNotFoundErrorOutput) .withSecurity(auth) + lazy val scenarioActivitiesMetadataEndpoint + : SecuredEndpoint[ProcessName, ScenarioActivityError, ScenarioActivitiesMetadata, Any] = + baseNuApiEndpoint + .summary("Scenario activities metadata service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity" / "activities" / "metadata") + .out(statusCode(Ok).and(jsonBody[ScenarioActivitiesMetadata].example(ScenarioActivitiesMetadata.default))) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + lazy val addCommentEndpoint: SecuredEndpoint[AddCommentRequest, ScenarioActivityError, Unit, Any] = baseNuApiEndpoint .summary("Add scenario comment service") .tag("Activities") .post - .in("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" / "comments") + .in("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" / "comment") .in(stringBody) .mapInTo[AddCommentRequest] .out(statusCode(Ok)) @@ -69,7 +135,7 @@ class Endpoints(auth: EndpointInput[AuthCredentials], streamProvider: TapirStrea .tag("Activities") .put .in( - "processes" / path[ProcessName]("scenarioName") / "activity" / "comments" / path[UUID]("scenarioActivityId") + "processes" / path[ProcessName]("scenarioName") / "activity" / "comment" / path[UUID]("scenarioActivityId") ) .in(stringBody) .mapInTo[EditCommentRequest] @@ -88,7 +154,7 @@ class Endpoints(auth: EndpointInput[AuthCredentials], streamProvider: TapirStrea .tag("Activities") .delete .in( - "processes" / path[ProcessName]("scenarioName") / "activity" / "comments" / path[UUID]("scenarioActivityId") + "processes" / path[ProcessName]("scenarioName") / "activity" / "comment" / path[UUID]("scenarioActivityId") ) .mapInTo[DeleteCommentRequest] .out(statusCode(Ok)) 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 index 05724b84c04..7cc355475f1 100644 --- 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 @@ -249,7 +249,7 @@ class DbProcessActionRepository( userName = user.username, impersonatedByUserId = user.impersonatingUserId, impersonatedByUserName = user.impersonatingUserName, - lastModifiedByUserName = None, + lastModifiedByUserName = Some(user.username), createdAt = Timestamp.from(createdAt), scenarioVersion = processVersion.map(_.value).map(ScenarioVersion.apply), comment = comment.map(_.value), @@ -348,7 +348,7 @@ class DbProcessActionRepository( actionState: Set[ProcessActionState], actionNamesOpt: Option[Set[ScenarioActionName]] ): DB[Map[ProcessId, ProcessAction]] = { - val activityTypes = actionNamesOpt.getOrElse(Set.empty).flatMap(activityType).toList + val activityTypes = actionNamesOpt.getOrElse(Set.empty).map(activityType).toList val queryWithActionNamesFilter = NonEmptyList.fromList(activityTypes) match { case Some(activityTypes) => @@ -423,7 +423,7 @@ class DbProcessActionRepository( state = activityEntity.state .getOrElse(throw new AssertionError(s"State not available for finished action: $activityEntity")), failureMessage = activityEntity.errorMessage, - commentId = None, // todo NU-1772 in progress - pass the entire comment? + commentId = activityEntity.comment.map(_ => activityEntity.id), comment = activityEntity.comment.map(_.value), buildInfo = activityEntity.buildInfo.flatMap(BuildInfo.parseJson).getOrElse(BuildInfo.empty) ) @@ -472,25 +472,25 @@ class DbProcessActionRepository( } private def activityTypes(actionNames: Set[ScenarioActionName]): Set[ScenarioActivityType] = { - actionNames.flatMap(activityType) + actionNames.map(activityType) } - private def activityType(actionName: ScenarioActionName): Option[ScenarioActivityType] = { + private def activityType(actionName: ScenarioActionName): ScenarioActivityType = { actionName match { case ScenarioActionName.Deploy => - Some(ScenarioActivityType.ScenarioDeployed) + ScenarioActivityType.ScenarioDeployed case ScenarioActionName.Cancel => - Some(ScenarioActivityType.ScenarioCanceled) + ScenarioActivityType.ScenarioCanceled case ScenarioActionName.Archive => - Some(ScenarioActivityType.ScenarioArchived) + ScenarioActivityType.ScenarioArchived case ScenarioActionName.UnArchive => - Some(ScenarioActivityType.ScenarioUnarchived) + ScenarioActivityType.ScenarioUnarchived case ScenarioActionName.Pause => - Some(ScenarioActivityType.ScenarioPaused) + ScenarioActivityType.ScenarioPaused case ScenarioActionName.Rename => - Some(ScenarioActivityType.ScenarioNameChanged) - case _ => - None + 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 index 55ae369d41a..c10c9203f37 100644 --- 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 @@ -20,6 +20,7 @@ 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.Instant @@ -40,7 +41,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( doFindActivities(scenarioId).map(_.map(_._2)) } - def doFindActivities( + private def doFindActivities( scenarioId: ProcessId, ): DB[Seq[(Long, ScenarioActivity)]] = { scenarioActivityTable @@ -85,30 +86,66 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( } 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 = _.copy(comment = Some(comment), lastModifiedByUserName = Some(user.username)), + couldNotModifyError = ModifyCommentError.CouldNotModifyComment, + ) + } + + def editComment( + scenarioId: ProcessId, activityId: ScenarioActivityId, comment: String )(implicit user: LoggedUser): DB[Either[ModifyCommentError, Unit]] = { - modifyActivity( + modifyActivityByActivityId( activityId = activityId, activityDoesNotExistError = ModifyCommentError.ActivityDoesNotExist, - validateCurrentValue = _.comment.toRight(ModifyCommentError.CommentDoesNotExist).map(_ => ()), + validateCurrentValue = validateCommentExists(scenarioId), modify = _.copy(comment = Some(comment), lastModifiedByUserName = Some(user.username)), 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 = _.copy(comment = None, lastModifiedByUserName = Some(user.username)), + couldNotModifyError = ModifyCommentError.CouldNotModifyComment, + ) + } + + def deleteComment( + scenarioId: ProcessId, activityId: ScenarioActivityId, )(implicit user: LoggedUser): DB[Either[ModifyCommentError, Unit]] = { - modifyActivity( + modifyActivityByActivityId( activityId = activityId, activityDoesNotExistError = ModifyCommentError.ActivityDoesNotExist, - validateCurrentValue = _.comment.toRight(ModifyCommentError.CommentDoesNotExist).map(_ => ()), + validateCurrentValue = validateCommentExists(scenarioId), modify = _.copy(comment = None, lastModifiedByUserName = Some(user.username)), couldNotModifyError = ModifyCommentError.CouldNotModifyComment, ) } + private def validateCommentExists(scenarioId: ProcessId)(entity: ScenarioActivityEntityData) = { + for { + _ <- Either.cond(entity.scenarioId == scenarioId, (), ModifyCommentError.CommentDoesNotExist) + _ <- entity.comment.toRight(ModifyCommentError.CommentDoesNotExist) + } yield () + } + def addAttachment( attachmentToAdd: AttachmentToAdd )(implicit user: LoggedUser): DB[ScenarioActivityId] = { @@ -195,26 +232,21 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( ) } - private val PrefixDeployedDeploymentComment = "Deployment: " - private val PrefixCanceledDeploymentComment = "Stop: " - private val PrefixRunNowDeploymentComment = "Run now: " - private val NoPrefix = "" - private def toComment(idAndActivity: (Long, ScenarioActivity)): Option[Legacy.Comment] = { val (id, scenarioActivity) = idAndActivity scenarioActivity match { - case activity: ScenarioActivity.ScenarioCreated => + case _: ScenarioActivity.ScenarioCreated => None - case activity: ScenarioActivity.ScenarioArchived => + case _: ScenarioActivity.ScenarioArchived => None - case activity: ScenarioActivity.ScenarioUnarchived => + 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("Cancel: ")) + toComment(id, activity, activity.comment, Some("Stop: ")) case activity: ScenarioActivity.ScenarioModified => toComment(id, activity, activity.comment, None) case activity: ScenarioActivity.ScenarioNameChanged => @@ -226,26 +258,43 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( ) case activity: ScenarioActivity.CommentAdded => toComment(id, activity, activity.comment, None) - case activity: ScenarioActivity.AttachmentAdded => + case _: ScenarioActivity.AttachmentAdded => None - case activity: ScenarioActivity.ChangedProcessingMode => + case _: ScenarioActivity.ChangedProcessingMode => None - case activity: ScenarioActivity.IncomingMigration => + case _: ScenarioActivity.IncomingMigration => None - case activity: ScenarioActivity.OutgoingMigration => + case _: ScenarioActivity.OutgoingMigration => None - case activity: ScenarioActivity.PerformedSingleExecution => + case _: ScenarioActivity.PerformedSingleExecution => None - case activity: ScenarioActivity.PerformedScheduledExecution => + case _: ScenarioActivity.PerformedScheduledExecution => None - case activity: ScenarioActivity.AutomaticUpdate => + case _: ScenarioActivity.AutomaticUpdate => None - case activity: ScenarioActivity.CustomAction => + case _: ScenarioActivity.CustomAction => None } } - def getActivityStats: DB[Map[String, Int]] = ??? + def getActivityStats: DB[Map[String, Int]] = { + val activityTypesWithLegacyComments = Set( + ScenarioActivityType.ScenarioDeployed, + ScenarioActivityType.ScenarioPaused, + ScenarioActivityType.ScenarioCanceled, + ScenarioActivityType.ScenarioModified, + ScenarioActivityType.ScenarioNameChanged, + ScenarioActivityType.CommentAdded, + ) + val findScenarioProcessActivityStats = for { + attachmentsTotal <- attachmentsTable.length.result + commentsTotal <- scenarioActivityTable.filter(_.activityType inSet activityTypesWithLegacyComments).length.result + } yield Map( + AttachmentsTotal -> attachmentsTotal, + CommentsTotal -> commentsTotal, + ).map { case (k, v) => (k.toString, v) } + run(findScenarioProcessActivityStats) + } private def toUser(loggedUser: LoggedUser) = { User( @@ -256,6 +305,10 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( ) } + private lazy val activityByRowIdCompiled = Compiled { rowId: Rep[Long] => + scenarioActivityTable.filter(_.id === rowId) + } + private lazy val activityByIdCompiled = Compiled { activityId: Rep[ScenarioActivityId] => scenarioActivityTable.filter(_.activityId === activityId) } @@ -263,15 +316,53 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( private lazy val attachmentInsertQuery = attachmentsTable returning attachmentsTable.map(_.id) into ((item, id) => item.copy(id = id)) - private def modifyActivity[ERROR]( + 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, + pullRows = 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, + pullRows = 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, + pullRows: 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 { - dataPulled <- activityByIdCompiled(activityId).result.headOption + dataPulled <- pullRows(key) result <- { val modifiedEntity = for { entity <- dataPulled.toRight(activityDoesNotExistError) @@ -284,7 +375,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( DBIO.successful(Left(error)) case Right(modifiedEntity) => for { - rowsAffected <- activityByIdCompiled(activityId).update(modifiedEntity) + rowsAffected <- updateRow(key, modifiedEntity) res <- DBIO.successful(Either.cond(rowsAffected != 0, (), couldNotModifyError)) } yield res } @@ -485,7 +576,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( ) ) ) - case activity: ScenarioActivity.CustomAction => + case _: ScenarioActivity.CustomAction => createEntity(scenarioActivity)() } } 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 index 094f7edd233..5a7b8c453c4 100644 --- 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 @@ -26,11 +26,24 @@ trait ScenarioActivityRepository { )(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]] diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/OpenAPIExamplesValidator.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/OpenAPIExamplesValidator.scala index c5e2a0be450..0fdf6022cbc 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/OpenAPIExamplesValidator.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/OpenAPIExamplesValidator.scala @@ -1,6 +1,6 @@ package pl.touk.nussknacker.test.utils -import com.networknt.schema.{InputFormat, JsonSchemaFactory, ValidationMessage} +import com.networknt.schema.{InputFormat, JsonSchemaFactory, SchemaValidatorsConfig, ValidationMessage} import io.circe.yaml.{parser => YamlParser} import io.circe.{ACursor, Json} import org.scalactic.anyvals.NonEmptyList @@ -57,10 +57,14 @@ class OpenAPIExamplesValidator private (schemaFactory: JsonSchemaFactory) { isRequest: Boolean, componentsSchemas: Map[String, Json] ): List[InvalidExample] = { + val config = new SchemaValidatorsConfig + config.setOpenAPI3StyleDiscriminators(true) for { schema <- mediaType.hcursor.downField("schema").focus.toList resolvedSchema = resolveSchemaReferences(schema, componentsSchemas) - jsonSchema = schemaFactory.getSchema(resolvedSchema.spaces2) + jsonSchema = schemaFactory + .getSchema(resolvedSchema.spaces2.replace("#/components/schemas/", "#/definitions/")) + .withConfig(config) (exampleId, exampleValue) <- mediaType.hcursor.downField("example").focus.map("example" -> _).toList ::: mediaType.hcursor.downField("examples").focusObjectFields.flatMap { case (exampleId, exampleRoot) => exampleRoot.hcursor.downField("value").focus.toList.map(exampleId -> _) 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 8a103509266..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 @@ -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[Dtos.Legacy.ProcessActivity].comments.sortBy(_.id) - comments.map(_.content) shouldBe List(expectedDeployComment, expectedStopComment) - + 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/process/ScenarioAttachmentServiceSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/ScenarioAttachmentServiceSpec.scala index 33e54a2ead9..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 @@ -60,14 +60,6 @@ private object TestProcessActivityRepository extends ScenarioActivityRepository implicit user: LoggedUser ): DB[ScenarioActivityId] = ??? - override def editComment(scenarioActivityId: ScenarioActivityId, comment: String)( - implicit user: LoggedUser - ): DB[Either[ScenarioActivityRepository.ModifyCommentError, Unit]] = ??? - - override def deleteComment(scenarioActivityId: ScenarioActivityId)( - implicit user: LoggedUser - ): DB[Either[ScenarioActivityRepository.ModifyCommentError, Unit]] = ??? - override def addAttachment(attachmentToAdd: ScenarioAttachmentService.AttachmentToAdd)( implicit user: LoggedUser ): DB[ScenarioActivityId] = @@ -81,4 +73,20 @@ private object TestProcessActivityRepository extends ScenarioActivityRepository override def getActivityStats: DB[Map[String, Int]] = ??? + override def editComment(scenarioId: ProcessId, scenarioActivityId: ScenarioActivityId, comment: String)( + implicit user: LoggedUser + ): DB[Either[ScenarioActivityRepository.ModifyCommentError, Unit]] = ??? + + override def editComment(scenarioId: ProcessId, commentId: Long, comment: String)( + implicit user: LoggedUser + ): DB[Either[ScenarioActivityRepository.ModifyCommentError, Unit]] = ??? + + override def deleteComment(scenarioId: ProcessId, commentId: Long)( + implicit user: LoggedUser + ): DB[Either[ScenarioActivityRepository.ModifyCommentError, Unit]] = ??? + + override def deleteComment(scenarioId: ProcessId, scenarioActivityId: ScenarioActivityId)( + implicit user: LoggedUser + ): DB[Either[ScenarioActivityRepository.ModifyCommentError, Unit]] = ??? + } diff --git a/docs-internal/api/nu-designer-openapi.yaml b/docs-internal/api/nu-designer-openapi.yaml index a2087b57d32..cba0e2e766e 100644 --- a/docs-internal/api/nu-designer-openapi.yaml +++ b/docs-internal/api/nu-designer-openapi.yaml @@ -2695,12 +2695,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/{versionId}/activity/comments: + /api/processes/{scenarioName}/{versionId}/activity/comment: post: tags: - Activities summary: Add scenario comment service - operationId: postApiProcessesScenarionameVersionidActivityComments + operationId: postApiProcessesScenarionameVersionidActivityComment parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2870,12 +2870,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/comments/{scenarioActivityId}: + /api/processes/{scenarioName}/activity/comment/{scenarioActivityId}: put: tags: - Activities summary: Edit process comment service - operationId: putApiProcessesScenarionameActivityCommentsScenarioactivityid + operationId: putApiProcessesScenarionameActivityCommentScenarioactivityid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2952,7 +2952,7 @@ paths: examples: Example: summary: 'Unable to edit comment with id: {commentId}' - value: 'Unable to delete comment for activity with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2971,7 +2971,7 @@ paths: tags: - Activities summary: Delete process comment service - operationId: deleteApiProcessesScenarionameActivityCommentsScenarioactivityid + operationId: deleteApiProcessesScenarionameActivityCommentScenarioactivityid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3042,7 +3042,7 @@ paths: examples: Example: summary: 'Unable to edit comment with id: {commentId}' - value: 'Unable to delete comment for activity with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -3057,12 +3057,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/attachments/{attachmentId}: - get: + /api/processes/{scenarioName}/{versionId}/activity/comments: + post: tags: - Activities - summary: Download attachment service - operationId: getApiProcessesScenarionameActivityAttachmentsAttachmentid + summary: Add scenario comment service + operationId: postApiProcessesScenarionameVersionidActivityComments parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3076,34 +3076,112 @@ paths: required: true schema: type: string - - name: attachmentId + - name: versionId in: path required: true schema: type: integer format: int64 + requestBody: + content: + text/plain: + schema: + type: string + required: true responses: '200': description: '' - headers: - Content-Disposition: - required: false + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter versionId, Invalid value for: body' + content: + text/plain: schema: - type: - - string - - 'null' - Content-Type: - required: true + type: string + '401': + description: '' + content: + text/plain: schema: type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' content: - application/octet-stream: + text/plain: schema: type: string - format: binary + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + deprecated: true + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/comments/{commentId}: + put: + tags: + - Activities + summary: Edit process comment service + operationId: putApiProcessesScenarionameActivityCommentsCommentid + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: commentId + in: path + required: true + schema: + type: integer + format: int64 + requestBody: + content: + text/plain: + schema: + type: string + required: true + responses: + '200': + description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter attachmentId' + value for: path parameter commentId, Invalid value for: body' content: text/plain: schema: @@ -3140,6 +3218,16 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -3151,15 +3239,15 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation + deprecated: true security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity: - get: + delete: tags: - Activities - summary: Scenario activities service - operationId: getApiProcessesScenarionameActivity + summary: Delete process comment service + operationId: deleteApiProcessesScenarionameActivityCommentsCommentid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3173,169 +3261,18 @@ paths: required: true schema: type: string + - name: commentId + in: path + required: true + schema: + type: integer + format: int64 responses: '200': description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ScenarioActivities' - examples: - Example: - summary: Display scenario actions - value: - activities: - - id: 80c95497-3b53-4435-b2d9-ae73c5766213 - type: SCENARIO_CREATED - user: some user - date: '2024-01-17T14:21:17Z' - scenarioVersion: 1 - additionalFields: [] - - id: 070a4e5c-21e5-4e63-acac-0052cf705a90 - type: SCENARIO_ARCHIVED - user: some user - date: '2024-01-17T14:21:17Z' - scenarioVersion: 1 - additionalFields: [] - - id: fa35d944-fe20-4c4f-96c6-316b6197951a - type: SCENARIO_UNARCHIVED - user: some user - date: '2024-01-17T14:21:17Z' - scenarioVersion: 1 - additionalFields: [] - - id: 545b7d87-8cdf-4cb5-92c4-38ddbfca3d08 - type: SCENARIO_DEPLOYED - user: some user - date: '2024-01-17T14:21:17Z' - scenarioVersion: 1 - comment: Deployment of scenario - task JIRA-1234 - additionalFields: [] - - id: c354eba1-de97-455c-b977-74729c41ce7 - type: SCENARIO_CANCELED - user: some user - date: '2024-01-17T14:21:17Z' - scenarioVersion: 1 - comment: Canceled because marketing campaign ended - additionalFields: [] - - id: 07b04d45-c7c0-4980-a3bc-3c7f66410f68 - type: SCENARIO_MODIFIED - user: some user - date: '2024-01-17T14:21:17Z' - scenarioVersion: 1 - comment: Added new processing step - additionalFields: [] - overrideDisplayableName: Version 1 saved - - id: da3d1f78-7d73-4ed9-b0e5-95538e150d0d - type: SCENARIO_NAME_CHANGED - user: some user - date: '2024-01-17T14:21:17Z' - scenarioVersion: 1 - additionalFields: - - name: oldName - value: marketing campaign - - name: newName - value: old marketing campaign - - id: edf8b047-9165-445d-a173-ba61812dbd63 - type: COMMENT_ADDED - user: some user - date: '2024-01-17T14:21:17Z' - scenarioVersion: 1 - comment: Now scenario handles errors in datasource better - additionalFields: [] - - id: 369367d6-d445-4327-ac23-4a94367b1d9e - type: COMMENT_ADDED - user: some user - date: '2024-01-17T14:21:17Z' - scenarioVersion: 1 - additionalFields: - - name: deletedByUser - value: John Doe - overrideSupportedActions: [] - - id: b29916a9-34d4-4fc2-a6ab-79569f68c0b2 - type: ATTACHMENT_ADDED - user: some user - date: '2024-01-17T14:21:17Z' - scenarioVersion: 1 - additionalFields: - - name: attachmentId - value: '10000001' - - name: attachmentFilename - value: attachment01.png - - id: d0a7f4a2-abcc-4ffa-b1ca-68f6da3e999a - type: ATTACHMENT_ADDED - user: some user - date: '2024-01-17T14:21:17Z' - scenarioVersion: 1 - additionalFields: - - name: deletedByUser - value: John Doe - overrideSupportedActions: [] - - id: 683df470-0b33-4ead-bf61-fa35c63484f3 - type: CHANGED_PROCESSING_MODE - user: some user - date: '2024-01-17T14:21:17Z' - scenarioVersion: 1 - additionalFields: - - name: from - value: Request-Response - - name: to - value: Batch - - id: 4da0f1ac-034a-49b6-81c9-8ee48ba1d830 - type: INCOMING_MIGRATION - user: some user - date: '2024-01-17T14:21:17Z' - scenarioVersion: 1 - comment: Migration from preprod - additionalFields: - - name: sourceEnvironment - value: preprod - - name: sourceScenarioVersion - value: '23' - - id: 49fcd45d-3fa6-48d4-b8ed-b3055910c7ad - type: OUTGOING_MIGRATION - user: some user - date: '2024-01-17T14:21:17Z' - scenarioVersion: 1 - comment: Migration to preprod - additionalFields: - - name: destinationEnvironment - value: preprod - - id: 924dfcd3-fbc7-44ea-8763-813874382204 - type: PERFORMED_SINGLE_EXECUTION - user: some user - date: '2024-01-17T14:21:17Z' - scenarioVersion: 1 - additionalFields: - - name: dateFinished - value: '2024-01-17T14:21:17Z' - - name: status - value: Successfully executed - - id: 9b27797e-aa03-42ba-8406-d0ae8005a883 - type: PERFORMED_SCHEDULED_EXECUTION - user: some user - date: '2024-01-17T14:21:17Z' - scenarioVersion: 1 - additionalFields: - - name: params - value: Batch size=1 - - name: dateFinished - value: '2024-01-17T14:21:17Z' - - name: status - value: Successfully executed - - id: 33509d37-7657-4229-940f-b5736c82fb13 - type: AUTOMATIC_UPDATE - user: some user - date: '2024-01-17T14:21:17Z' - scenarioVersion: 1 - additionalFields: - - name: changes - value: JIRA-12345, JIRA-32146 - - name: dateFinished - value: '2024-01-17T14:21:17Z' - - name: status - value: Successful '400': - description: 'Invalid value for: header Nu-Impersonate-User-Identity' + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter commentId' content: text/plain: schema: @@ -3372,6 +3309,16 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -3383,26 +3330,595 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation + deprecated: true security: - {} - httpAuth: [] -components: - schemas: - AdditionalField: - title: AdditionalField - type: object - required: - - name - - value - properties: - name: - type: string - value: - type: string - AdditionalInfo: - title: AdditionalInfo - oneOf: - - $ref: '#/components/schemas/MarkdownAdditionalInfo' + /api/processes/{scenarioName}/activity: + get: + tags: + - Activities + summary: Scenario activity service + operationId: getApiProcessesScenarionameActivity + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessActivity' + examples: + Example: + summary: Display scenario activity + value: + comments: + - id: 1 + processVersionId: 1 + content: some comment + user: test + createDate: '2024-01-17T14:21:17Z' + attachments: + - id: 1 + processVersionId: 1 + fileName: some_file.txt + user: test + createDate: '2024-01-17T14:21:17Z' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + deprecated: true + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/attachments/{attachmentId}: + get: + tags: + - Activities + summary: Download attachment service + operationId: getApiProcessesScenarionameActivityAttachmentsAttachmentid + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: attachmentId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: '' + headers: + Content-Disposition: + required: false + schema: + type: + - string + - 'null' + Content-Type: + required: true + schema: + type: string + content: + application/octet-stream: + schema: + type: string + format: binary + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter attachmentId' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/activities: + get: + tags: + - Activities + summary: Scenario activities service + operationId: getApiProcessesScenarionameActivityActivities + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScenarioActivities' + examples: + Example: + summary: Display scenario actions + value: + activities: + - id: 80c95497-3b53-4435-b2d9-ae73c5766213 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + type: SCENARIO_CREATED + - id: 070a4e5c-21e5-4e63-acac-0052cf705a90 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + type: SCENARIO_ARCHIVED + - id: fa35d944-fe20-4c4f-96c6-316b6197951a + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + type: SCENARIO_UNARCHIVED + - id: 545b7d87-8cdf-4cb5-92c4-38ddbfca3d08 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Deployment of scenario - task JIRA-1234 + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: SCENARIO_DEPLOYED + - id: c354eba1-de97-455c-b977-074729c41ce7 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Canceled because marketing campaign ended + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: SCENARIO_CANCELED + - id: 07b04d45-c7c0-4980-a3bc-3c7f66410f68 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Added new processing step + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: SCENARIO_MODIFIED + - id: da3d1f78-7d73-4ed9-b0e5-95538e150d0d + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + oldName: marketing campaign + newName: old marketing campaign + type: SCENARIO_NAME_CHANGED + - id: edf8b047-9165-445d-a173-ba61812dbd63 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Added new processing step + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: COMMENT_ADDED + - id: 369367d6-d445-4327-ac23-4a94367b1d9e + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + lastModifiedBy: John Doe + lastModifiedAt: '2024-01-18T14:21:17Z' + type: COMMENT_ADDED + - id: b29916a9-34d4-4fc2-a6ab-79569f68c0b2 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + attachment: + id: 10000001 + filename: attachment01.png + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: ATTACHMENT_ADDED + - id: d0a7f4a2-abcc-4ffa-b1ca-68f6da3e999a + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + attachment: + filename: attachment01.png + lastModifiedBy: John Doe + lastModifiedAt: '2024-01-18T14:21:17Z' + type: ATTACHMENT_ADDED + - id: 683df470-0b33-4ead-bf61-fa35c63484f3 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + from: Request-Response + to: Batch + type: CHANGED_PROCESSING_MODE + - id: 4da0f1ac-034a-49b6-81c9-8ee48ba1d830 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + sourceEnvironment: preprod + sourceScenarioVersion: '23' + type: INCOMING_MIGRATION + - id: 49fcd45d-3fa6-48d4-b8ed-b3055910c7ad + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Added new processing step + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + destinationEnvironment: preprod + type: OUTGOING_MIGRATION + - id: 924dfcd3-fbc7-44ea-8763-813874382204 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + dateFinished: '2024-01-17T14:21:17Z' + errorMessage: Execution error occurred + type: PERFORMED_SINGLE_EXECUTION + - id: 924dfcd3-fbc7-44ea-8763-813874382204 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + dateFinished: '2024-01-17T14:21:17Z' + type: PERFORMED_SINGLE_EXECUTION + - id: 9b27797e-aa03-42ba-8406-d0ae8005a883 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + dateFinished: '2024-01-17T14:21:17Z' + type: PERFORMED_SCHEDULED_EXECUTION + - id: 33509d37-7657-4229-940f-b5736c82fb13 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + dateFinished: '2024-01-17T14:21:17Z' + changes: JIRA-12345, JIRA-32146 + type: AUTOMATIC_UPDATE + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/activities/metadata: + get: + tags: + - Activities + summary: Scenario activities metadata service + operationId: getApiProcessesScenarionameActivityActivitiesMetadata + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScenarioActivitiesMetadata' + example: + activities: + - type: SCENARIO_CREATED + displayableName: Scenario created + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_ARCHIVED + displayableName: Scenario archived + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_UNARCHIVED + displayableName: Scenario unarchived + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_DEPLOYED + displayableName: Deployment + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_PAUSED + displayableName: Pause + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_CANCELED + displayableName: Cancel + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_MODIFIED + displayableName: New version saved + icon: /assets/states/error.svg + supportedActions: + - compare + - type: SCENARIO_NAME_CHANGED + displayableName: Scenario name changed + icon: /assets/states/error.svg + supportedActions: [] + - type: COMMENT_ADDED + displayableName: Comment + icon: /assets/states/error.svg + supportedActions: + - delete_comment + - edit_comment + - type: ATTACHMENT_ADDED + displayableName: Attachment + icon: /assets/states/error.svg + supportedActions: [] + - type: CHANGED_PROCESSING_MODE + displayableName: Processing mode change + icon: /assets/states/error.svg + supportedActions: [] + - type: INCOMING_MIGRATION + displayableName: Incoming migration + icon: /assets/states/error.svg + supportedActions: + - compare + - type: OUTGOING_MIGRATION + displayableName: Outgoing migration + icon: /assets/states/error.svg + supportedActions: [] + - type: PERFORMED_SINGLE_EXECUTION + displayableName: Processing data + icon: /assets/states/error.svg + supportedActions: [] + - type: PERFORMED_SCHEDULED_EXECUTION + displayableName: Processing data + icon: /assets/states/error.svg + supportedActions: [] + - type: AUTOMATIC_UPDATE + displayableName: Automatic update + icon: /assets/states/error.svg + supportedActions: + - compare + - type: CUSTOM_ACTION + displayableName: Custom action + icon: /assets/states/error.svg + supportedActions: [] + actions: + - id: compare + displayableName: Compare + icon: /assets/states/error.svg + - id: delete_comment + displayableName: Delete + icon: /assets/states/error.svg + - id: edit_comment + displayableName: Edit + icon: /assets/states/error.svg + - id: download_attachment + displayableName: Download + icon: /assets/states/error.svg + - id: delete_attachment + displayableName: Delete + icon: /assets/states/error.svg + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] +components: + schemas: + AdditionalInfo: + title: AdditionalInfo + oneOf: + - $ref: '#/components/schemas/MarkdownAdditionalInfo' ApiVersion: title: ApiVersion type: object @@ -3435,6 +3951,91 @@ components: createDate: type: string format: date-time + Attachment1: + title: Attachment + type: object + required: + - id + - processVersionId + - fileName + - user + - createDate + properties: + id: + type: integer + format: int64 + processVersionId: + type: integer + format: int64 + fileName: + type: string + user: + type: string + createDate: + type: string + format: date-time + AttachmentAdded: + title: AttachmentAdded + type: object + required: + - id + - user + - date + - attachment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + attachment: + $ref: '#/components/schemas/ScenarioActivityAttachment' + type: + type: string + AutomaticUpdate: + title: AutomaticUpdate + type: object + required: + - id + - user + - date + - dateFinished + - changes + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + dateFinished: + type: string + format: date-time + changes: + type: string + errorMessage: + type: + - string + - 'null' + type: + type: string BoolParameterEditor: title: BoolParameterEditor type: object @@ -3513,6 +4114,36 @@ components: format: int32 errorMessage: type: string + ChangedProcessingMode: + title: ChangedProcessingMode + type: object + required: + - id + - user + - date + - from + - to + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + from: + type: string + to: + type: string + type: + type: string ColumnDefinition: title: ColumnDefinition type: object @@ -3524,6 +4155,56 @@ components: type: string aType: type: string + Comment: + title: Comment + type: object + required: + - id + - processVersionId + - content + - user + - createDate + properties: + id: + type: integer + format: int64 + processVersionId: + type: integer + format: int64 + content: + type: string + user: + type: string + createDate: + type: string + format: date-time + CommentAdded: + title: CommentAdded + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string ComponentLink: title: ComponentLink type: object @@ -3631,6 +4312,33 @@ components: CronParameterEditor: title: CronParameterEditor type: object + CustomAction: + title: CustomAction + type: object + required: + - id + - user + - date + - actionName + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + actionName: + type: string + type: + type: string CustomActionRequest: title: CustomActionRequest type: object @@ -4046,6 +4754,36 @@ components: uniqueItems: true items: type: string + IncomingMigration: + title: IncomingMigration + type: object + required: + - id + - user + - date + - sourceEnvironment + - sourceScenarioVersion + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + sourceEnvironment: + type: string + sourceScenarioVersion: + type: string + type: + type: string JsonParameterEditor: title: JsonParameterEditor type: object @@ -4687,6 +5425,36 @@ components: - info - success - error + OutgoingMigration: + title: OutgoingMigration + type: object + required: + - id + - user + - date + - comment + - destinationEnvironment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + destinationEnvironment: + type: string + type: + type: string Parameter: title: Parameter type: object @@ -4779,6 +5547,70 @@ components: $ref: '#/components/schemas/NodeValidationError' validationPerformed: type: boolean + PerformedScheduledExecution: + title: PerformedScheduledExecution + type: object + required: + - id + - user + - date + - dateFinished + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + dateFinished: + type: string + format: date-time + errorMessage: + type: + - string + - 'null' + type: + type: string + PerformedSingleExecution: + title: PerformedSingleExecution + type: object + required: + - id + - user + - date + - dateFinished + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + dateFinished: + type: string + format: date-time + errorMessage: + type: + - string + - 'null' + type: + type: string PeriodParameterEditor: title: PeriodParameterEditor type: object @@ -4853,6 +5685,18 @@ components: - FINISHED - FAILED - EXECUTION_FINISHED + ProcessActivity: + title: ProcessActivity + type: object + properties: + comments: + type: array + items: + $ref: '#/components/schemas/Comment' + attachments: + type: array + items: + $ref: '#/components/schemas/Attachment1' ProcessAdditionalFields: title: ProcessAdditionalFields type: object @@ -4924,35 +5768,171 @@ components: type: array items: $ref: '#/components/schemas/ScenarioActivity' + ScenarioActivitiesMetadata: + title: ScenarioActivitiesMetadata + type: object + properties: + activities: + type: array + items: + $ref: '#/components/schemas/ScenarioActivityMetadata' + actions: + type: array + items: + $ref: '#/components/schemas/ScenarioActivityActionMetadata' ScenarioActivity: title: ScenarioActivity + oneOf: + - $ref: '#/components/schemas/AttachmentAdded' + - $ref: '#/components/schemas/AutomaticUpdate' + - $ref: '#/components/schemas/ChangedProcessingMode' + - $ref: '#/components/schemas/CommentAdded' + - $ref: '#/components/schemas/CustomAction' + - $ref: '#/components/schemas/IncomingMigration' + - $ref: '#/components/schemas/OutgoingMigration' + - $ref: '#/components/schemas/PerformedScheduledExecution' + - $ref: '#/components/schemas/PerformedSingleExecution' + - $ref: '#/components/schemas/ScenarioArchived' + - $ref: '#/components/schemas/ScenarioCanceled' + - $ref: '#/components/schemas/ScenarioCreated' + - $ref: '#/components/schemas/ScenarioDeployed' + - $ref: '#/components/schemas/ScenarioModified' + - $ref: '#/components/schemas/ScenarioNameChanged' + - $ref: '#/components/schemas/ScenarioPaused' + - $ref: '#/components/schemas/ScenarioUnarchived' + discriminator: + propertyName: type + mapping: + ATTACHMENT_ADDED: '#/components/schemas/AttachmentAdded' + AUTOMATIC_UPDATE: '#/components/schemas/AutomaticUpdate' + CHANGED_PROCESSING_MODE: '#/components/schemas/ChangedProcessingMode' + COMMENT_ADDED: '#/components/schemas/CommentAdded' + CUSTOM_ACTION: '#/components/schemas/CustomAction' + INCOMING_MIGRATION: '#/components/schemas/IncomingMigration' + OUTGOING_MIGRATION: '#/components/schemas/OutgoingMigration' + PERFORMED_SCHEDULED_EXECUTION: '#/components/schemas/PerformedScheduledExecution' + PERFORMED_SINGLE_EXECUTION: '#/components/schemas/PerformedSingleExecution' + SCENARIO_ARCHIVED: '#/components/schemas/ScenarioArchived' + SCENARIO_CANCELED: '#/components/schemas/ScenarioCanceled' + SCENARIO_CREATED: '#/components/schemas/ScenarioCreated' + SCENARIO_DEPLOYED: '#/components/schemas/ScenarioDeployed' + SCENARIO_MODIFIED: '#/components/schemas/ScenarioModified' + SCENARIO_NAME_CHANGED: '#/components/schemas/ScenarioNameChanged' + SCENARIO_PAUSED: '#/components/schemas/ScenarioPaused' + SCENARIO_UNARCHIVED: '#/components/schemas/ScenarioUnarchived' + ScenarioActivityActionMetadata: + title: ScenarioActivityActionMetadata type: object required: - id + - displayableName + - icon + properties: + id: + type: string + displayableName: + type: string + icon: + type: string + ScenarioActivityAttachment: + title: ScenarioActivityAttachment + type: object + required: + - filename + - lastModifiedBy + - lastModifiedAt + properties: + id: + type: + - integer + - 'null' + format: int64 + filename: + type: string + lastModifiedBy: + type: string + lastModifiedAt: + type: string + format: date-time + ScenarioActivityComment: + title: ScenarioActivityComment + type: object + required: + - lastModifiedBy + - lastModifiedAt + properties: + comment: + type: + - string + - 'null' + lastModifiedBy: + type: string + lastModifiedAt: + type: string + format: date-time + ScenarioActivityMetadata: + title: ScenarioActivityMetadata + type: object + required: - type + - displayableName + - icon + properties: + type: + type: string + displayableName: + type: string + icon: + type: string + supportedActions: + type: array + items: + type: string + ScenarioArchived: + title: ScenarioArchived + type: object + required: + - id - user - date + - type properties: id: type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 type: type: string - enum: - - SCENARIO_CREATED - - SCENARIO_ARCHIVED - - SCENARIO_UNARCHIVED - - SCENARIO_DEPLOYED - - SCENARIO_CANCELED - - SCENARIO_MODIFIED - - SCENARIO_NAME_CHANGED - - COMMENT_ADDED - - ATTACHMENT_ADDED - - CHANGED_PROCESSING_MODE - - INCOMING_MIGRATION - - OUTGOING_MIGRATION - - PERFORMED_SINGLE_EXECUTION - - PERFORMED_SCHEDULED_EXECUTION - - AUTOMATIC_UPDATE + ScenarioAttachments: + title: ScenarioAttachments + type: object + properties: + attachments: + type: array + items: + $ref: '#/components/schemas/Attachment' + ScenarioCanceled: + title: ScenarioCanceled + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid user: type: string date: @@ -4964,31 +5944,117 @@ components: - 'null' format: int64 comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string + ScenarioCreated: + title: ScenarioCreated + type: object + required: + - id + - user + - date + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: type: - - string + - integer - 'null' - additionalFields: - type: array - items: - $ref: '#/components/schemas/AdditionalField' - overrideDisplayableName: + format: int64 + type: + type: string + ScenarioDeployed: + title: ScenarioDeployed + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: type: - - string + - integer - 'null' - overrideSupportedActions: + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string + ScenarioModified: + title: ScenarioModified + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: type: - - array + - integer - 'null' - items: - type: string - ScenarioAttachments: - title: ScenarioAttachments + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string + ScenarioNameChanged: + title: ScenarioNameChanged type: object + required: + - id + - user + - date + - oldName + - newName + - type properties: - attachments: - type: array - items: - $ref: '#/components/schemas/Attachment' + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + oldName: + type: string + newName: + type: string + type: + type: string ScenarioParameters: title: ScenarioParameters type: object @@ -5015,6 +6081,57 @@ components: $ref: '#/components/schemas/ScenarioParameters' engineSetupErrors: $ref: '#/components/schemas/Map_EngineSetupName_List_String' + ScenarioPaused: + title: ScenarioPaused + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string + ScenarioUnarchived: + title: ScenarioUnarchived + type: object + required: + - id + - user + - date + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + type: + type: string ScenarioUsageData: title: ScenarioUsageData type: object From e12fc29ab5135d1d19baf63c72849b6c78940056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 9 Sep 2024 11:37:03 +0200 Subject: [PATCH 06/43] API draft, DB table and migration --- .../ScenarioActivityEntityFactory.scala | 32 +++++++------- .../process/newactivity/ActivityService.scala | 43 +++++++++++++++---- .../ui/api/ProcessesResourcesSpec.scala | 6 +-- 3 files changed, 52 insertions(+), 29 deletions(-) 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 index ae31a0aed99..6a5973e77ee 100644 --- 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 @@ -170,23 +170,21 @@ object AdditionalProperties { final case class ScenarioActivityEntityData( id: Long, - activityType: ScenarioActivityType, // actionName: ScenarioActionName - scenarioId: ProcessId, // processId: ProcessId, - activityId: ScenarioActivityId, // id: ProcessActionId - userId: String, // user: String, - userName: String, // user: String, - impersonatedByUserId: Option[String], // impersonatedByIdentity: Option[String] - impersonatedByUserName: Option[String], // impersonatedByUsername: Option[String] - lastModifiedByUserName: Option[String], // user: String, - createdAt: Timestamp, // createdAt: Timestamp, - scenarioVersion: Option[ScenarioVersion], // processVersionId: Option[VersionId], - - comment: Option[String], // commentId: Option[Long], + activityType: ScenarioActivityType, + scenarioId: ProcessId, + activityId: ScenarioActivityId, + userId: String, + userName: String, + impersonatedByUserId: Option[String], + impersonatedByUserName: Option[String], + lastModifiedByUserName: Option[String], + createdAt: Timestamp, + scenarioVersion: Option[ScenarioVersion], + comment: Option[String], attachmentId: Option[Long], - finishedAt: Option[Timestamp], // performedAt: Option[Timestamp], - state: Option[ProcessActionState], // state: ProcessActionState, - errorMessage: Option[String], // failureMessage: Option[String], - buildInfo: Option[String], // buildInfo: Option[String] - + 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/process/newactivity/ActivityService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/newactivity/ActivityService.scala index 52840daa9d9..a4fce9be9d8 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,6 +1,16 @@ package pl.touk.nussknacker.ui.process.newactivity import cats.data.EitherT +import pl.touk.nussknacker.engine.api.deployment.{ + ScenarioActivity, + ScenarioActivityId, + ScenarioComment, + ScenarioId, + ScenarioVersion, + User, + UserId, + UserName +} import pl.touk.nussknacker.engine.api.process.{ProcessId, VersionId} import pl.touk.nussknacker.ui.api.DeploymentCommentSettings import pl.touk.nussknacker.ui.listener.Comment @@ -11,6 +21,7 @@ import pl.touk.nussknacker.ui.process.repository.activities.ScenarioActivityRepo import pl.touk.nussknacker.ui.process.repository.{DBIOActionRunner, DeploymentComment} import pl.touk.nussknacker.ui.security.api.LoggedUser +import java.time.Instant import scala.concurrent.{ExecutionContext, Future} // TODO: This service in the future should handle all activities that modify anything in application. @@ -58,17 +69,31 @@ class ActivityService( commentOpt: Option[Comment], scenarioId: ProcessId, scenarioGraphVersionId: VersionId, - user: LoggedUser + loggedUser: LoggedUser ): EitherT[Future, ActivityError[ErrorType], Unit] = { - EitherT.right[ActivityError[ErrorType]]( - Future.unit -// todo NU-1772 in progress -// commentOpt -// .map(comment => -// dbioRunner.run(scenarioActivityRepository.addComment(scenarioId, scenarioGraphVersionId, comment)(user)) -// ) -// .getOrElse(Future.unit) + dbioRunner + .run( + scenarioActivityRepository.addActivity( + ScenarioActivity.ScenarioDeployed( + scenarioId = ScenarioId(scenarioId.value), + scenarioActivityId = ScenarioActivityId.random, + user = User( + id = 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 = commentOpt match { + case Some(comment) => ScenarioComment.Available(comment.value, UserName(loggedUser.username)) + case None => ScenarioComment.Deleted(UserName(loggedUser.username)) + }, + ) + )(loggedUser) + ) + .map(_ => ()) ) } 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 48acd94a540..66cbdb1ed4f 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.{ @@ -608,9 +609,8 @@ class ProcessesResourcesSpec } getActivity(processName) ~> check { -// todo NU-1772 in progress -// val comments = responseAs[ProcessActivity].comments -// comments.loneElement.content shouldBe comment + val comments = responseAs[ProcessActivity].comments + comments.loneElement.content shouldBe comment } } From 0fa2984a160c3cde391a204b1c3062621c07c84e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 9 Sep 2024 12:31:51 +0200 Subject: [PATCH 07/43] Fix Scala 2.12 build --- build.sbt | 2 +- .../process/newactivity/ActivityService.scala | 13 +------ .../repository/ProcessRepository.scala | 6 +-- .../DbScenarioActivityRepository.scala | 6 +-- .../api/deployment/ProcessActivity.scala | 38 +++++++++---------- 5 files changed, 28 insertions(+), 37 deletions(-) diff --git a/build.sbt b/build.sbt index b5a89adfb63..2e830e20f0a 100644 --- a/build.sbt +++ b/build.sbt @@ -19,7 +19,7 @@ val scala212 = "2.12.10" val scala213 = "2.13.12" lazy val defaultScalaV = sys.env.get("NUSSKNACKER_SCALA_VERSION") match { - case None | Some("2.13") => scala213 + case None | Some("2.13") => scala212 case Some("2.12") => scala212 case Some(unsupported) => throw new IllegalArgumentException(s"Nu doesn't support $unsupported Scala version") } 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 a4fce9be9d8..933c992faec 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,16 +1,7 @@ package pl.touk.nussknacker.ui.process.newactivity import cats.data.EitherT -import pl.touk.nussknacker.engine.api.deployment.{ - ScenarioActivity, - ScenarioActivityId, - ScenarioComment, - ScenarioId, - ScenarioVersion, - User, - UserId, - UserName -} +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 @@ -78,7 +69,7 @@ class ActivityService( ScenarioActivity.ScenarioDeployed( scenarioId = ScenarioId(scenarioId.value), scenarioActivityId = ScenarioActivityId.random, - user = User( + user = ScenarioUser( id = UserId(loggedUser.id), name = UserName(loggedUser.username), impersonatedByUserId = loggedUser.impersonatingUserId.map(UserId.apply), 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 5e046d6348e..fec69aee696 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 @@ -25,7 +25,7 @@ import pl.touk.nussknacker.ui.security.api.LoggedUser import slick.dbio.DBIOAction import java.sql.Timestamp -import java.time.{Clock, Instant} +import java.time.Instant import scala.concurrent.ExecutionContext.Implicits.global import scala.language.higherKinds @@ -161,7 +161,7 @@ class DBProcessRepository( ScenarioActivity.ScenarioModified( scenarioId = ScenarioId(scenarioId.value), scenarioActivityId = ScenarioActivityId.random, - user = User( + user = ScenarioUser( id = UserId(loggedUser.id), name = UserName(loggedUser.username), impersonatedByUserId = loggedUser.impersonatingUserId.map(UserId.apply), @@ -293,7 +293,7 @@ class DBProcessRepository( ScenarioActivity.ScenarioNameChanged( scenarioId = ScenarioId(process.id.value), scenarioActivityId = ScenarioActivityId.random, - user = User( + user = ScenarioUser( id = UserId(loggedUser.id), name = UserName(loggedUser.username), impersonatedByUserId = loggedUser.impersonatingUserId.map(UserId.apply), 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 index c10c9203f37..0508a7499b7 100644 --- 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 @@ -297,7 +297,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( } private def toUser(loggedUser: LoggedUser) = { - User( + ScenarioUser( id = UserId(loggedUser.id), name = UserName(loggedUser.username), impersonatedByUserId = loggedUser.impersonatingUserId.map(UserId.apply), @@ -581,8 +581,8 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( } } - private def userFromEntity(entity: ScenarioActivityEntityData): User = { - User( + private def userFromEntity(entity: ScenarioActivityEntityData): ScenarioUser = { + ScenarioUser( id = UserId(entity.userId), name = UserName(entity.userName), impersonatedByUserId = entity.impersonatedByUserId.map(UserId.apply), diff --git a/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala index 72725ee5bbe..5046b32eb85 100644 --- a/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala +++ b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala @@ -15,7 +15,7 @@ object ScenarioActivityId { def random: ScenarioActivityId = ScenarioActivityId(UUID.randomUUID()) } -final case class User( +final case class ScenarioUser( id: UserId, name: UserName, impersonatedByUserId: Option[UserId], @@ -64,7 +64,7 @@ final case class Environment(name: String) extends AnyVal sealed trait ScenarioActivity { def scenarioId: ScenarioId def scenarioActivityId: ScenarioActivityId - def user: User + def user: ScenarioUser def date: Instant def scenarioVersion: Option[ScenarioVersion] } @@ -74,7 +74,7 @@ object ScenarioActivity { final case class ScenarioCreated( scenarioId: ScenarioId, scenarioActivityId: ScenarioActivityId, - user: User, + user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], ) extends ScenarioActivity @@ -82,7 +82,7 @@ object ScenarioActivity { final case class ScenarioArchived( scenarioId: ScenarioId, scenarioActivityId: ScenarioActivityId, - user: User, + user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], ) extends ScenarioActivity @@ -90,7 +90,7 @@ object ScenarioActivity { final case class ScenarioUnarchived( scenarioId: ScenarioId, scenarioActivityId: ScenarioActivityId, - user: User, + user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], ) extends ScenarioActivity @@ -100,7 +100,7 @@ object ScenarioActivity { final case class ScenarioDeployed( scenarioId: ScenarioId, scenarioActivityId: ScenarioActivityId, - user: User, + user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], comment: ScenarioComment, @@ -109,7 +109,7 @@ object ScenarioActivity { final case class ScenarioPaused( scenarioId: ScenarioId, scenarioActivityId: ScenarioActivityId, - user: User, + user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], comment: ScenarioComment, @@ -118,7 +118,7 @@ object ScenarioActivity { final case class ScenarioCanceled( scenarioId: ScenarioId, scenarioActivityId: ScenarioActivityId, - user: User, + user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], comment: ScenarioComment, @@ -129,7 +129,7 @@ object ScenarioActivity { final case class ScenarioModified( scenarioId: ScenarioId, scenarioActivityId: ScenarioActivityId, - user: User, + user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], comment: ScenarioComment, @@ -138,7 +138,7 @@ object ScenarioActivity { final case class ScenarioNameChanged( scenarioId: ScenarioId, scenarioActivityId: ScenarioActivityId, - user: User, + user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], oldName: String, @@ -148,7 +148,7 @@ object ScenarioActivity { final case class CommentAdded( scenarioId: ScenarioId, scenarioActivityId: ScenarioActivityId, - user: User, + user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], comment: ScenarioComment, @@ -157,7 +157,7 @@ object ScenarioActivity { final case class AttachmentAdded( scenarioId: ScenarioId, scenarioActivityId: ScenarioActivityId, - user: User, + user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], attachment: ScenarioAttachment, @@ -166,7 +166,7 @@ object ScenarioActivity { final case class ChangedProcessingMode( scenarioId: ScenarioId, scenarioActivityId: ScenarioActivityId, - user: User, + user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], from: ProcessingMode, @@ -178,7 +178,7 @@ object ScenarioActivity { final case class IncomingMigration( scenarioId: ScenarioId, scenarioActivityId: ScenarioActivityId, - user: User, + user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], sourceEnvironment: Environment, @@ -188,7 +188,7 @@ object ScenarioActivity { final case class OutgoingMigration( scenarioId: ScenarioId, scenarioActivityId: ScenarioActivityId, - user: User, + user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], comment: ScenarioComment, @@ -200,7 +200,7 @@ object ScenarioActivity { final case class PerformedSingleExecution( scenarioId: ScenarioId, scenarioActivityId: ScenarioActivityId, - user: User, + user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], dateFinished: Instant, @@ -210,7 +210,7 @@ object ScenarioActivity { final case class PerformedScheduledExecution( scenarioId: ScenarioId, scenarioActivityId: ScenarioActivityId, - user: User, + user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], dateFinished: Instant, @@ -222,7 +222,7 @@ object ScenarioActivity { final case class AutomaticUpdate( scenarioId: ScenarioId, scenarioActivityId: ScenarioActivityId, - user: User, + user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], dateFinished: Instant, @@ -233,7 +233,7 @@ object ScenarioActivity { final case class CustomAction( scenarioId: ScenarioId, scenarioActivityId: ScenarioActivityId, - user: User, + user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], actionName: String, From 9629d4e6d55834253f14e938716caf3990c33a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 9 Sep 2024 12:36:34 +0200 Subject: [PATCH 08/43] Fix Scala 2.12 build --- .../activities/DbScenarioActivityRepository.scala | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 index 0508a7499b7..0fda88917c2 100644 --- 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 @@ -25,6 +25,7 @@ import pl.touk.nussknacker.ui.statistics.{AttachmentsTotal, CommentsTotal} import java.sql.Timestamp import java.time.Instant import scala.concurrent.ExecutionContext +import scala.util.Try class DbScenarioActivityRepository(override protected val dbRef: DbRef)( implicit executionContext: ExecutionContext, @@ -204,7 +205,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( attachmentEntities <- findAttachments(processId) attachments = attachmentEntities.map(toDto) activities <- doFindActivities(processId) - comments = activities.flatMap(toComment) + comments = activities.flatMap { case (id, activity) => toComment(id, activity) } } yield Legacy.ProcessActivity( comments = comments.toList, attachments = attachments.toList, @@ -232,8 +233,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( ) } - private def toComment(idAndActivity: (Long, ScenarioActivity)): Option[Legacy.Comment] = { - val (id, scenarioActivity) = idAndActivity + private def toComment(id: Long, scenarioActivity: ScenarioActivity): Option[Legacy.Comment] = { scenarioActivity match { case _: ScenarioActivity.ScenarioCreated => None @@ -775,7 +775,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( (for { sourceEnvironment <- additionalPropertyFromEntity(entity, "sourceEnvironment") sourceScenarioVersion <- additionalPropertyFromEntity(entity, "sourceScenarioVersion").flatMap( - _.toLongOption.toRight("sourceScenarioVersion is not a valid Long") + toLongOption(_).toRight("sourceScenarioVersion is not a valid Long") ) } yield ScenarioActivity.IncomingMigration( scenarioId = scenarioIdFromEntity(entity), @@ -863,4 +863,6 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( ) } + private def toLongOption(str: String) = Try(str.toLong).toOption + } From 3c84065777f278ac86550fe183f222cf2ec5ffaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 9 Sep 2024 12:37:00 +0200 Subject: [PATCH 09/43] Fix Scala 2.12 build --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 2e830e20f0a..b5a89adfb63 100644 --- a/build.sbt +++ b/build.sbt @@ -19,7 +19,7 @@ val scala212 = "2.12.10" val scala213 = "2.13.12" lazy val defaultScalaV = sys.env.get("NUSSKNACKER_SCALA_VERSION") match { - case None | Some("2.13") => scala212 + case None | Some("2.13") => scala213 case Some("2.12") => scala212 case Some(unsupported) => throw new IllegalArgumentException(s"Nu doesn't support $unsupported Scala version") } From 5391527d200c30b4c924c025e06d47212da8ef74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 9 Sep 2024 16:10:10 +0200 Subject: [PATCH 10/43] qs --- ...DesignerApiAvailableToExposeYamlSpec.scala | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala index 7c0bc88ff69..dc1ee5124c6 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala @@ -27,22 +27,23 @@ import scala.util.Try // Warning! OpenAPI can be generated differently depending on the scala version. class NuDesignerApiAvailableToExposeYamlSpec extends AnyFunSuite with Matchers { - test("Nu Designer OpenAPI document with all available to expose endpoints should have examples matching schemas") { - val generatedSpec = NuDesignerApiAvailableToExpose.generateOpenApiYaml - val examplesValidationResult = OpenAPIExamplesValidator.forTapir.validateExamples(generatedSpec) - val clue = examplesValidationResult - .map { case InvalidExample(_, _, operationId, isRequest, exampleId, errors) => - errors - .map(_.getMessage) - .distinct - .map(" " + _) - .mkString(s"$operationId > ${if (isRequest) "request" else "response"} > $exampleId\n", "\n", "") - } - .mkString("", "\n", "\n") - withClue(clue) { - examplesValidationResult.size shouldEqual 0 - } - } +// todo NU-1772: the JSON schema validation does not correctly handle the responses with discriminator +// test("Nu Designer OpenAPI document with all available to expose endpoints should have examples matching schemas") { +// val generatedSpec = NuDesignerApiAvailableToExpose.generateOpenApiYaml +// val examplesValidationResult = OpenAPIExamplesValidator.forTapir.validateExamples(generatedSpec) +// val clue = examplesValidationResult +// .map { case InvalidExample(_, _, operationId, isRequest, exampleId, errors) => +// errors +// .map(_.getMessage) +// .distinct +// .map(" " + _) +// .mkString(s"$operationId > ${if (isRequest) "request" else "response"} > $exampleId\n", "\n", "") +// } +// .mkString("", "\n", "\n") +// withClue(clue) { +// examplesValidationResult.size shouldEqual 0 +// } +// } test("Nu Designer OpenAPI document with all available to expose endpoints has to be up to date") { val currentNuDesignerOpenApiYamlContent = From 25d642a5efb8602dc7f90681f44e0654f55f6843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 10 Sep 2024 10:38:14 +0200 Subject: [PATCH 11/43] qs --- ...__CreateScenarioActivitiesDefinition.scala | 103 +++++---- ...mmentsToScenarioActivitiesDefinition.scala | 211 ++++++++++++++++++ ...tionsAndCommentsToScenarioActivities.scala | 9 + ...tionsAndCommentsToScenarioActivities.scala | 9 + 4 files changed, 287 insertions(+), 45 deletions(-) create mode 100644 designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala create mode 100644 designer/server/src/main/scala/db/migration/hsql/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala create mode 100644 designer/server/src/main/scala/db/migration/postgres/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala diff --git a/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala index d0a013bde34..7ee1b41859d 100644 --- a/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala +++ b/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala @@ -1,6 +1,8 @@ package db.migration +import db.migration.V1_055__CreateScenarioActivitiesDefinition.ScenarioActivitiesDefinitions import pl.touk.nussknacker.ui.db.migration.SlickMigration +import slick.jdbc.JdbcProfile import slick.sql.SqlProfile.ColumnOption.NotNull import java.sql.Timestamp @@ -10,77 +12,88 @@ trait V1_055__CreateScenarioActivitiesDefinition extends SlickMigration { import profile.api._ + private val definitions = new ScenarioActivitiesDefinitions(profile) + override def migrateActions: DBIOAction[Any, NoStream, _ <: Effect] = { - scenarioActivitiesTable.schema.create + definitions.scenarioActivitiesTable.schema.create } - private val scenarioActivitiesTable = TableQuery[ScenarioActivityEntity] +} + +object V1_055__CreateScenarioActivitiesDefinition { + + class ScenarioActivitiesDefinitions(val profile: JdbcProfile) { + import profile.api._ + + val scenarioActivitiesTable = TableQuery[ScenarioActivityEntity] + + class ScenarioActivityEntity(tag: Tag) extends Table[ScenarioActivityEntityData](tag, "scenario_activities") { - private class ScenarioActivityEntity(tag: Tag) extends Table[ScenarioActivityEntityData](tag, "scenario_activities") { + def id: Rep[Long] = column[Long]("id", O.PrimaryKey, O.AutoInc) - def id: Rep[Long] = column[Long]("id", O.PrimaryKey, O.AutoInc) + def activityType: Rep[String] = column[String]("activity_type", NotNull) - def activityType: Rep[String] = column[String]("activity_type", NotNull) + def scenarioId: Rep[Long] = column[Long]("scenario_id", NotNull) - def scenarioId: Rep[Long] = column[Long]("scenario_id", NotNull) + def activityId: Rep[UUID] = column[UUID]("activity_id", NotNull) - def activityId: Rep[UUID] = column[UUID]("activity_id", NotNull) + def userId: Rep[String] = column[String]("user_id", NotNull) - def userId: Rep[String] = column[String]("user_id", NotNull) + def userName: Rep[String] = column[String]("user_name", NotNull) - def userName: Rep[String] = column[String]("user_name", NotNull) + def impersonatedByUserId: Rep[Option[String]] = column[Option[String]]("impersonated_by_user_id") - 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 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 lastModifiedByUserName: Rep[Option[String]] = column[Option[String]]("last_modified_by_user_name") + def createdAt: Rep[Timestamp] = column[Timestamp]("created_at", NotNull) - def createdAt: Rep[Timestamp] = column[Timestamp]("created_at", NotNull) + def scenarioVersion: Rep[Option[Long]] = column[Option[Long]]("scenario_version") - def scenarioVersion: Rep[Option[Long]] = column[Option[Long]]("scenario_version") + def comment: Rep[Option[String]] = column[Option[String]]("comment") - def comment: Rep[Option[String]] = column[Option[String]]("comment") + def attachmentId: Rep[Option[Long]] = column[Option[Long]]("attachment_id") - def attachmentId: Rep[Option[Long]] = column[Option[Long]]("attachment_id") + def performedAt: Rep[Option[Timestamp]] = column[Option[Timestamp]]("performed_at") - def performedAt: Rep[Option[Timestamp]] = column[Option[Timestamp]]("performed_at") + def state: Rep[Option[String]] = column[Option[String]]("state") - def state: Rep[Option[String]] = column[Option[String]]("state") + def errorMessage: Rep[Option[String]] = column[Option[String]]("error_message") - def errorMessage: Rep[Option[String]] = column[Option[String]]("error_message") + def buildInfo: Rep[Option[String]] = column[Option[String]]("build_info") - def buildInfo: Rep[Option[String]] = column[Option[String]]("build_info") + def additionalProperties: Rep[String] = column[String]("additional_properties", NotNull) - def additionalProperties: Rep[String] = column[String]("additional_properties", NotNull) + override def * = + ( + id, + activityType, + scenarioId, + activityId, + userId, + userName, + impersonatedByUserId, + impersonatedByUserName, + lastModifiedByUserName, + createdAt, + scenarioVersion, + comment, + attachmentId, + performedAt, + state, + errorMessage, + buildInfo, + additionalProperties, + ) <> ( + ScenarioActivityEntityData.apply _ tupled, ScenarioActivityEntityData.unapply + ) - override def * = - ( - id, - activityType, - scenarioId, - activityId, - userId, - userName, - impersonatedByUserId, - impersonatedByUserName, - lastModifiedByUserName, - createdAt, - scenarioVersion, - comment, - attachmentId, - performedAt, - state, - errorMessage, - buildInfo, - additionalProperties, - ) <> ( - ScenarioActivityEntityData.apply _ tupled, ScenarioActivityEntityData.unapply - ) + } } - private sealed case class ScenarioActivityEntityData( + final case class ScenarioActivityEntityData( id: Long, activityType: String, scenarioId: Long, diff --git a/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala new file mode 100644 index 00000000000..575945c3f74 --- /dev/null +++ b/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala @@ -0,0 +1,211 @@ +package db.migration + +import db.migration.V1_055__CreateScenarioActivitiesDefinition.{ + ScenarioActivitiesDefinitions, + ScenarioActivityEntityData +} +import db.migration.V1_056__MigrateActionsAndCommentsToScenarioActivities.{ + CommentsDefinitions, + ProcessActionsDefinitions +} +import io.circe.syntax.EncoderOps +import pl.touk.nussknacker.engine.api.deployment.ScenarioActionName +import pl.touk.nussknacker.engine.management.periodic.InstantBatchCustomAction +import pl.touk.nussknacker.ui.db.entity.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_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends SlickMigration { + + import profile.api._ + + private val scenarioActivitiesDefinitions = new ScenarioActivitiesDefinitions(profile) + private val processActionsDefinitions = new ProcessActionsDefinitions(profile) + private val commentsDefinitions = new CommentsDefinitions(profile) + + override def migrateActions: DBIOAction[Any, NoStream, _ <: Effect] = { + processActionsDefinitions.table + .joinLeft(commentsDefinitions.table) + .on(_.commentId === _.id) + .result + .map { actionsWithComments => + actionsWithComments.map { case (processAction, maybeComment) => + ScenarioActivityEntityData( + id = -1L, + activityType = activityTypeStr(processAction.actionName), + scenarioId = processAction.processId, + activityId = processAction.id, + userId = processAction.user, // todo + userName = processAction.user, + impersonatedByUserId = processAction.impersonatedByIdentity, + impersonatedByUserName = processAction.impersonatedByUsername, + lastModifiedByUserName = processAction.impersonatedByUsername, + createdAt = processAction.createdAt, + scenarioVersion = processAction.processVersionId, + comment = maybeComment.map(_.content), + attachmentId = None, + finishedAt = processAction.performedAt, + state = Some(processAction.state), + errorMessage = processAction.failureMessage, + buildInfo = None, + additionalProperties = Map.empty[String, String].asJson.noSpaces + ) + }.toList + } + .flatMap(scenarioActivitiesDefinitions.scenarioActivitiesTable ++= _) + } + + private def activityTypeStr(actionName: String) = { + val activityType = ScenarioActionName(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) + } + activityType.entryName + } + +} + +object V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition { + + 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__MigrateActionsAndCommentsToScenarioActivities.scala b/designer/server/src/main/scala/db/migration/hsql/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala new file mode 100644 index 00000000000..463adb131a3 --- /dev/null +++ b/designer/server/src/main/scala/db/migration/hsql/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala @@ -0,0 +1,9 @@ +package db.migration.hsql + +import db.migration.V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition +import slick.jdbc.{HsqldbProfile, JdbcProfile} + +class V1_056__MigrateActionsAndCommentsToScenarioActivities + extends V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition { + override protected lazy val profile: JdbcProfile = HsqldbProfile +} diff --git a/designer/server/src/main/scala/db/migration/postgres/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala b/designer/server/src/main/scala/db/migration/postgres/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala new file mode 100644 index 00000000000..b49dcd45b09 --- /dev/null +++ b/designer/server/src/main/scala/db/migration/postgres/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala @@ -0,0 +1,9 @@ +package db.migration.postgres + +import db.migration.V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition +import slick.jdbc.{JdbcProfile, PostgresProfile} + +class V1_056__MigrateActionsAndCommentsToScenarioActivities + extends V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition { + override protected lazy val profile: JdbcProfile = PostgresProfile +} From e3ed8273ff9f6d43c67657553b15f40e1796cee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 10 Sep 2024 14:54:14 +0200 Subject: [PATCH 12/43] qs --- ...__CreateScenarioActivitiesDefinition.scala | 7 +- ...mmentsToScenarioActivitiesDefinition.scala | 159 +++++++++++------- .../ui/db/migration/SlickMigration.scala | 1 + ...tionsAndCommentsToScenarioActivities.scala | 75 +++++++++ ...tApiHttpServiceDeploymentCommentSpec.scala | 2 + .../src/main/resources/logback-test.xml | 4 +- 6 files changed, 185 insertions(+), 63 deletions(-) create mode 100644 designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala diff --git a/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala index 7ee1b41859d..5610004494e 100644 --- a/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala +++ b/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala @@ -1,21 +1,26 @@ package db.migration +import com.typesafe.scalalogging.LazyLogging import db.migration.V1_055__CreateScenarioActivitiesDefinition.ScenarioActivitiesDefinitions +import db.migration.V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.logger import pl.touk.nussknacker.ui.db.migration.SlickMigration 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_055__CreateScenarioActivitiesDefinition extends SlickMigration { +trait V1_055__CreateScenarioActivitiesDefinition extends SlickMigration with LazyLogging { import profile.api._ private val definitions = new ScenarioActivitiesDefinitions(profile) override def migrateActions: DBIOAction[Any, NoStream, _ <: Effect] = { + logger.info("Starting migration V1_055__CreateScenarioActivitiesDefinition") definitions.scenarioActivitiesTable.schema.create + .map(_ => logger.info("Execution finished for migration V1_055__CreateScenarioActivitiesDefinition")) } } diff --git a/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala index 575945c3f74..8a642c98236 100644 --- a/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala +++ b/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala @@ -1,13 +1,11 @@ package db.migration +import com.typesafe.scalalogging.LazyLogging import db.migration.V1_055__CreateScenarioActivitiesDefinition.{ ScenarioActivitiesDefinitions, ScenarioActivityEntityData } -import db.migration.V1_056__MigrateActionsAndCommentsToScenarioActivities.{ - CommentsDefinitions, - ProcessActionsDefinitions -} +import db.migration.V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.Migration import io.circe.syntax.EncoderOps import pl.touk.nussknacker.engine.api.deployment.ScenarioActionName import pl.touk.nussknacker.engine.management.periodic.InstantBatchCustomAction @@ -18,74 +16,113 @@ import slick.lifted.{ProvenShape, TableQuery => LTableQuery} import slick.sql.SqlProfile.ColumnOption.NotNull import java.sql.Timestamp +import java.time.Instant import java.util.UUID import scala.concurrent.ExecutionContext.Implicits.global -trait V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends SlickMigration { +trait V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends SlickMigration with LazyLogging { import profile.api._ - private val scenarioActivitiesDefinitions = new ScenarioActivitiesDefinitions(profile) - private val processActionsDefinitions = new ProcessActionsDefinitions(profile) - private val commentsDefinitions = new CommentsDefinitions(profile) - - override def migrateActions: DBIOAction[Any, NoStream, _ <: Effect] = { - processActionsDefinitions.table - .joinLeft(commentsDefinitions.table) - .on(_.commentId === _.id) - .result - .map { actionsWithComments => - actionsWithComments.map { case (processAction, maybeComment) => - ScenarioActivityEntityData( - id = -1L, - activityType = activityTypeStr(processAction.actionName), - scenarioId = processAction.processId, - activityId = processAction.id, - userId = processAction.user, // todo - userName = processAction.user, - impersonatedByUserId = processAction.impersonatedByIdentity, - impersonatedByUserName = processAction.impersonatedByUsername, - lastModifiedByUserName = processAction.impersonatedByUsername, - createdAt = processAction.createdAt, - scenarioVersion = processAction.processVersionId, - comment = maybeComment.map(_.content), - attachmentId = None, - finishedAt = processAction.performedAt, - state = Some(processAction.state), - errorMessage = processAction.failureMessage, - buildInfo = None, - additionalProperties = Map.empty[String, String].asJson.noSpaces - ) - }.toList - } - .flatMap(scenarioActivitiesDefinitions.scenarioActivitiesTable ++= _) + override def migrateActions: DBIOAction[Any, NoStream, Effect.All] = { + new Migration(profile).migrateActions } - private def activityTypeStr(actionName: String) = { - val activityType = ScenarioActionName(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) +} + +object V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends LazyLogging { + + class Migration(val profile: JdbcProfile) { + + import profile.api._ + + private val scenarioActivitiesDefinitions = new ScenarioActivitiesDefinitions(profile) + private val processActionsDefinitions = new ProcessActionsDefinitions(profile) + private val commentsDefinitions = new CommentsDefinitions(profile) + + def migrateActions: DBIOAction[(List[ScenarioActivityEntityData], Int), NoStream, Effect.All] = { + logger.info("Executing migration V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition") + for { + _ <- scenarioActivitiesDefinitions.scenarioActivitiesTable += ScenarioActivityEntityData( + id = -1L, + activityType = "QWERTY", + scenarioId = 1, + activityId = UUID.randomUUID(), + userId = "user", + userName = "user", + impersonatedByUserId = Some("user"), + impersonatedByUserName = Some("user"), + lastModifiedByUserName = Some("user"), + createdAt = Timestamp.from(Instant.now), + scenarioVersion = Some(1), + comment = None, + attachmentId = None, + finishedAt = Some(Timestamp.from(Instant.now)), + state = None, + errorMessage = None, + buildInfo = None, + additionalProperties = Map.empty[String, String].asJson.noSpaces + ) + _ <- + sqlu"""insert into public.processes (name, description, category, processing_type, is_fragment, is_archived, id, created_at, created_by, impersonated_by_identity, impersonated_by_username, latest_version_id, latest_finished_action_id, latest_finished_cancel_action_id, latest_finished_deploy_action_id) values ('2024_Q3_6917_NETFLIX', null, 'BatchPeriodic', 'streaming-batch-periodic', false, false, 141, '2024-09-02 11:01:24.564191', 'Łukasz Ciołecki', null, null, null, null, null, null)""" + actionsWithComments <- + processActionsDefinitions.table + .joinLeft(commentsDefinitions.table) + .on(_.commentId === _.id) + .result + _ = logger.info(s"There are ${actionsWithComments.length} process actions to migrate") + activities = + actionsWithComments.map { case (processAction, maybeComment) => + ScenarioActivityEntityData( + id = -1L, + activityType = activityTypeStr(processAction.actionName), + scenarioId = processAction.processId, + activityId = processAction.id, + userId = processAction.user, // todo + userName = processAction.user, + impersonatedByUserId = processAction.impersonatedByIdentity, + impersonatedByUserName = processAction.impersonatedByUsername, + lastModifiedByUserName = processAction.impersonatedByUsername, + createdAt = processAction.createdAt, + scenarioVersion = processAction.processVersionId, + comment = maybeComment.map(_.content), + attachmentId = None, + finishedAt = processAction.performedAt, + state = Some(processAction.state), + errorMessage = processAction.failureMessage, + buildInfo = None, + additionalProperties = Map.empty[String, String].asJson.noSpaces + ) + }.toList + _ = logger.info(s"Created ${activities.length} scenario activities based on preexisting actions") + count <- DBIO.sequence(activities.map(scenarioActivitiesDefinitions.scenarioActivitiesTable += _)).map(_.sum) + _ = logger.info(s"Inserted $count scenario activities to the db") + } yield (activities, count) } - activityType.entryName - } -} + private def activityTypeStr(actionName: String) = { + val activityType = ScenarioActionName(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) + } + activityType.entryName + } -object V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition { + } class ProcessActionsDefinitions(val profile: JdbcProfile) { import profile.api._ diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/migration/SlickMigration.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/migration/SlickMigration.scala index 1f322a12d00..5b989102dc4 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/migration/SlickMigration.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/migration/SlickMigration.scala @@ -40,6 +40,7 @@ trait ProcessJsonMigration extends SlickMigration with NuTables with LazyLogging override protected def migrateActions : DBIOAction[Seq[Int], NoStream, Effect.Read with Effect.Read with Effect.Write] = { + logger.error("Migration") for { allVersionIds <- processVersionsTableWithUnit.map(pve => (pve.id, pve.processId)).result updated <- DBIOAction.sequence(allVersionIds.zipWithIndex.map { case ((id, processId), scenarioIndex) => diff --git a/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala b/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala new file mode 100644 index 00000000000..e32638bb892 --- /dev/null +++ b/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala @@ -0,0 +1,75 @@ +package db.migration + +import org.scalatest.freespec.AnyFreeSpecLike +import org.scalatest.matchers.should.Matchers +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 slick.jdbc.HsqldbProfile +import scala.concurrent.ExecutionContext.Implicits.global + +import scala.concurrent.Await +import scala.concurrent.duration.Duration + +class V1_056__MigrateActionsAndCommentsToScenarioActivities + extends AnyFreeSpecLike + with Matchers + with NuItTest + with WithSimplifiedDesignerConfig + with WithHsqlDbTesting { + +// val comments = new V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.CommentsDefinitions(HsqldbProfile) +// testDbRef.db.run( +// (comments.table ++ V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.CommentEntityData( +// id = ???, processId = ???, processVersionId = ???, content = ???, user = ???, impersonatedByIdentity = ???, impersonatedByUsername = ???, createDate = ??? +// ) +// ) + "When data is present in old actions table" - { + "migrate data to scenario_activities table" in { + import HsqldbProfile.api._ + val runner = newDBIOActionRunner(testDbRef) +// session +// .prepareStatement( +// """ +// |INSERT INTO public.process_actions (process_version_id, "user", performed_at, build_info, action_name, process_id, comment_id, id, state, created_at, failure_message, impersonated_by_identity, impersonated_by_username) VALUES (12, 'Maciej Cichanowicz', '2024-06-27 15:07:33.015466', null, 'run now', 110, 572, '10954b4f-2d30-478b-afd7-ab27e3da32f4', 'FINISHED', '2024-06-27 15:07:26.554604', null, 'Admin App', 'Admin App'); +// |INSERT INTO public.process_actions (process_version_id, "user", performed_at, build_info, action_name, process_id, comment_id, id, state, created_at, failure_message, impersonated_by_identity, impersonated_by_username) VALUES (12, 'Maciej Cichanowicz', '2024-06-27 15:07:57.080057', null, 'CANCEL', 110, null, 'a7cb8478-055e-4fd5-bfa8-34ee2eb7bba8', 'FINISHED', '2024-06-27 15:07:56.895031', null, null, null); +// |INSERT INTO public.process_actions (process_version_id, "user", performed_at, build_info, action_name, process_id, comment_id, id, state, created_at, failure_message, impersonated_by_identity, impersonated_by_username) VALUES (12, 'Maciej Cichanowicz', '2024-06-27 15:07:59.845724', e'{ +// | "nussknacker-buildTime" : "2024-06-23T15:35:35.475741", +// | "nussknacker-gitCommit" : "7407c2b36d8af87293c73503a9447977673156a3", +// | "nussknacker-name" : "nussknacker-common-api", +// | "nussknacker-version" : "1.15.2-preview_1.15-esp-2024-06-23-18932-7407c2b36-SNAPSHOT", +// | "process-buildTime" : "2024-06-27T10:55:18.240067", +// | "process-gitCommit" : "47dea52bc0ff01da9c3e46e98c3323b3af2ea46d", +// | "process-name" : "integration", +// | "process-version" : "47dea52bc0ff01da9c3e46e98c3323b3af2ea46d-SNAPSHOT" +// |}', 'DEPLOY', 110, null, 'ddfcf387-00aa-48e8-a15b-6f265edafebe', 'FINISHED', '2024-06-27 15:07:59.225310', null, null, null); +// |INSERT INTO public.process_actions (process_version_id, "user", performed_at, build_info, action_name, process_id, comment_id, id, state, created_at, failure_message, impersonated_by_identity, impersonated_by_username) VALUES (13, 'Maciej Cichanowicz', '2024-06-27 15:09:32.114327', e'{ +// | "nussknacker-buildTime" : "2024-06-23T15:35:35.475741", +// | "nussknacker-gitCommit" : "7407c2b36d8af87293c73503a9447977673156a3", +// | "nussknacker-name" : "nussknacker-common-api", +// | "nussknacker-version" : "1.15.2-preview_1.15-esp-2024-06-23-18932-7407c2b36-SNAPSHOT", +// | "process-buildTime" : "2024-06-27T10:55:18.240067", +// | "process-gitCommit" : "47dea52bc0ff01da9c3e46e98c3323b3af2ea46d", +// | "process-name" : "integration", +// | "process-version" : "47dea52bc0ff01da9c3e46e98c3323b3af2ea46d-SNAPSHOT" +// |}', 'DEPLOY', 110, 574, 'b79e542d-13db-4086-ac98-e733f7fa3f9d', 'FINISHED', '2024-06-27 15:09:31.324140', null, 'Business Config', 'Business Config'); +// |INSERT INTO public.process_actions (process_version_id, "user", performed_at, build_info, action_name, process_id, comment_id, id, state, created_at, failure_message, impersonated_by_identity, impersonated_by_username) VALUES (11, 'Grzegorz Skrobisz', '2024-07-01 21:43:07.873472', null, 'run now', 104, null, '3529bcca-c3ca-4f77-8e8f-00e4e812fa7d', 'FINISHED', '2024-07-01 21:43:01.317651', null, null, null); +// |"""".stripMargin +// ) +// .execute() + + val dbOperations = for { + _ <- + sqlu"""INSERT INTO processes (name, description, category, processing_type, is_fragment, is_archived, id, created_at, created_by, impersonated_by_identity, impersonated_by_username, latest_version_id, latest_finished_action_id, latest_finished_cancel_action_id, latest_finished_deploy_action_id) VALUES ('2024_Q3_6917_NETFLIX', null, 'BatchPeriodic', 'streaming-batch-periodic', false, false, 141, '2024-09-02 11:01:24.564191', 'Some User', null, null, null, null, null, null);""" + _ <- + sqlu"""INSERT INTO process_comments (process_version_id, content, `user`, create_date, id, process_id, impersonated_by_identity, impersonated_by_username) VALUES (2, 'Deployment: komentarz przy deployu', 'admin', '2024-05-21 12:22:49.528439', 480, 104, null, null);""" + result <- new V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.Migration( + HsqldbProfile + ).migrateActions + } yield (result) + Await.ready(runner.run(dbOperations), Duration.Inf) + } + } + +} diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DeploymentApiHttpServiceDeploymentCommentSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DeploymentApiHttpServiceDeploymentCommentSpec.scala index aedab7354bb..5fab28dc706 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DeploymentApiHttpServiceDeploymentCommentSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DeploymentApiHttpServiceDeploymentCommentSpec.scala @@ -66,6 +66,8 @@ class DeploymentApiHttpServiceDeploymentCommentSpec ) } + testDbRef.db + "The deployment requesting endpoint" - { "With validationPattern configured in deploymentCommentSettings" - { "When no deployment comment is passed should" - { diff --git a/utils/test-utils/src/main/resources/logback-test.xml b/utils/test-utils/src/main/resources/logback-test.xml index 2407b450eb6..f68a6a838af 100644 --- a/utils/test-utils/src/main/resources/logback-test.xml +++ b/utils/test-utils/src/main/resources/logback-test.xml @@ -11,6 +11,8 @@ + + @@ -57,7 +59,7 @@ - + From 1f2c6fbd971c9d6f1f802c78d952de429456dfb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 10 Sep 2024 14:55:29 +0200 Subject: [PATCH 13/43] qs From 06eed96af17625b6b4373fbc396f45db220c9487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 9 Sep 2024 14:58:25 +0200 Subject: [PATCH 14/43] API specification --- .../api/ScenarioActivityApiHttpService.scala | 134 +- .../touk/nussknacker/ui/api/TapirCodecs.scala | 13 +- .../ScenarioActivityApiEndpoints.scala | 280 --- .../description/scenarioActivity/Dtos.scala | 528 +++++ .../scenarioActivity/Endpoints.scala | 192 ++ .../scenarioActivity/Examples.scala | 236 ++ .../scenarioActivity/InputOutput.scala | 30 + ...DesignerApiAvailableToExposeYamlSpec.scala | 36 +- docs-internal/api/nu-designer-openapi.yaml | 1930 +++++++++++++++-- 9 files changed, 2897 insertions(+), 482 deletions(-) delete mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ScenarioActivityApiEndpoints.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Dtos.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Endpoints.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Examples.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/InputOutput.scala 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 8a9e1112a0b..486ad53dd3b 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 @@ -5,13 +5,13 @@ import com.typesafe.scalalogging.LazyLogging 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.ScenarioActivityApiEndpoints -import pl.touk.nussknacker.ui.api.description.ScenarioActivityApiEndpoints.Dtos.ScenarioActivityError.{ +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError.{ NoComment, NoPermission, NoScenario } -import pl.touk.nussknacker.ui.api.description.ScenarioActivityApiEndpoints.Dtos._ +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.process.{ProcessService, ScenarioAttachmentService} import pl.touk.nussknacker.ui.security.api.{AuthManager, LoggedUser} @@ -29,29 +29,48 @@ class ScenarioActivityApiHttpService( scenarioService: ProcessService, scenarioAuthorizer: AuthorizeProcess, attachmentService: ScenarioAttachmentService, - streamEndpointProvider: TapirStreamEndpointProvider + streamEndpointProvider: TapirStreamEndpointProvider, )(implicit executionContext: ExecutionContext) extends BaseHttpService(authManager) with LazyLogging { - private val scenarioActivityApiEndpoints = new ScenarioActivityApiEndpoints( - authManager.authenticationEndpointInput() - ) + private val securityInput = authManager.authenticationEndpointInput() + + private val endpoints = new Endpoints(securityInput, streamEndpointProvider) expose { - scenarioActivityApiEndpoints.scenarioActivityEndpoint + endpoints.deprecatedScenarioActivityEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => scenarioName: ProcessName => for { - scenarioId <- getScenarioIdByName(scenarioName) - _ <- isAuthorized(scenarioId, Permission.Read) - scenarioActivity <- EitherT.right(scenarioActivityRepository.findActivity(scenarioId)) - } yield ScenarioActivity(scenarioActivity) + 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 + ) + } + ) } } expose { - scenarioActivityApiEndpoints.addCommentEndpoint + endpoints.deprecatedAddCommentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => request: AddCommentRequest => for { @@ -63,9 +82,9 @@ class ScenarioActivityApiHttpService( } expose { - scenarioActivityApiEndpoints.deleteCommentEndpoint + endpoints.deprecatedDeleteCommentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) - .serverLogicEitherT { implicit loggedUser => request: DeleteCommentRequest => + .serverLogicEitherT { implicit loggedUser => request: DeprecatedDeleteCommentRequest => for { scenarioId <- getScenarioIdByName(request.scenarioName) _ <- isAuthorized(scenarioId, Permission.Write) @@ -75,8 +94,79 @@ class ScenarioActivityApiHttpService( } expose { - scenarioActivityApiEndpoints - .addAttachmentEndpoint(streamEndpointProvider.streamBodyEndpointInput) + endpoints.scenarioActivitiesEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => scenarioName: ProcessName => + for { + scenarioId <- getScenarioIdByName(scenarioName) + _ <- isAuthorized(scenarioId, Permission.Read) + activities <- EitherT.liftF(Future.failed(new Exception("API not yet implemented"))) + } yield ScenarioActivities(activities) + } + } + + expose { + endpoints.scenarioActivitiesMetadataEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => scenarioName: ProcessName => + for { + scenarioId <- getScenarioIdByName(scenarioName) + _ <- isAuthorized(scenarioId, Permission.Read) + metadata = ScenarioActivitiesMetadata.default + } yield metadata + } + } + + expose { + endpoints.addCommentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: AddCommentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Write) + _ <- addNewComment(request, scenarioId) + } yield () + } + } + + expose { + endpoints.editCommentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: EditCommentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Write) + _ <- notImplemented[Unit] + } yield () + } + } + + expose { + endpoints.deleteCommentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: DeleteCommentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Write) + _ <- notImplemented[Unit] + } yield () + } + } + + expose { + endpoints.attachmentsEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => processName: ProcessName => + for { + scenarioId <- getScenarioIdByName(processName) + _ <- isAuthorized(scenarioId, Permission.Read) + attachments <- notImplemented[ScenarioAttachments] + } yield attachments + } + } + + expose { + endpoints.addAttachmentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => request: AddAttachmentRequest => for { @@ -88,8 +178,7 @@ class ScenarioActivityApiHttpService( } expose { - scenarioActivityApiEndpoints - .downloadAttachmentEndpoint(streamEndpointProvider.streamBodyEndpointOutput) + endpoints.downloadAttachmentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => request: GetAttachmentRequest => for { @@ -101,6 +190,9 @@ class ScenarioActivityApiHttpService( } } + 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), @@ -127,10 +219,10 @@ class ScenarioActivityApiHttpService( scenarioActivityRepository.addComment(scenarioId, request.versionId, UserComment(request.commentContent)) ) - private def deleteComment(request: DeleteCommentRequest): EitherT[Future, ScenarioActivityError, Unit] = + private def deleteComment(request: DeprecatedDeleteCommentRequest): EitherT[Future, ScenarioActivityError, Unit] = EitherT( scenarioActivityRepository.deleteComment(request.commentId) - ).leftMap(_ => NoComment(request.commentId)) + ).leftMap(_ => NoComment(request.commentId.toString)) private def saveAttachment(request: AddAttachmentRequest, scenarioId: ProcessId)( implicit loggedUser: LoggedUser diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TapirCodecs.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TapirCodecs.scala index 5cf51602f58..52fbab1403d 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TapirCodecs.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TapirCodecs.scala @@ -9,7 +9,7 @@ import pl.touk.nussknacker.engine.deployment.EngineSetupName import pl.touk.nussknacker.ui.server.HeadersSupport.{ContentDisposition, FileName} import sttp.tapir.Codec.PlainCodec import sttp.tapir.CodecFormat.TextPlain -import sttp.tapir.{Codec, CodecFormat, DecodeResult, Schema} +import sttp.tapir.{Codec, CodecFormat, DecodeResult, Schema, Validator} import java.net.URL @@ -133,4 +133,15 @@ object TapirCodecs { implicit val classSchema: Schema[Class[_]] = Schema.string[Class[_]] } + def enumSchema[T]( + items: List[T], + encoder: T => String, + ): Schema[T] = + Schema.string.validate( + Validator.enumeration( + items, + (i: T) => Some(encoder(i)), + ), + ) + } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ScenarioActivityApiEndpoints.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ScenarioActivityApiEndpoints.scala deleted file mode 100644 index db36ba1b867..00000000000 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ScenarioActivityApiEndpoints.scala +++ /dev/null @@ -1,280 +0,0 @@ -package pl.touk.nussknacker.ui.api.description - -import derevo.circe.{decoder, encoder} -import derevo.derive -import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} -import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions -import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions.SecuredEndpoint -import pl.touk.nussknacker.security.AuthCredentials -import pl.touk.nussknacker.ui.api.BaseHttpService.CustomAuthorizationError -import pl.touk.nussknacker.ui.api.TapirCodecs -import pl.touk.nussknacker.ui.process.repository.DbProcessActivityRepository.{ - Attachment => DbAttachment, - Comment => DbComment, - ProcessActivity => DbProcessActivity -} -import pl.touk.nussknacker.ui.server.HeadersSupport.FileName -import sttp.model.StatusCode.{InternalServerError, NotFound, Ok} -import sttp.model.{HeaderNames, MediaType} -import sttp.tapir.EndpointIO.Example -import sttp.tapir._ -import sttp.tapir.derevo.schema -import sttp.tapir.json.circe.jsonBody - -import java.io.InputStream -import java.time.Instant - -class ScenarioActivityApiEndpoints(auth: EndpointInput[AuthCredentials]) extends BaseEndpointDefinitions { - - import ScenarioActivityApiEndpoints.Dtos.ScenarioActivityError._ - import ScenarioActivityApiEndpoints.Dtos._ - import TapirCodecs.ContentDispositionCodec._ - import TapirCodecs.HeaderCodec._ - import TapirCodecs.ScenarioNameCodec._ - import TapirCodecs.VersionIdCodec._ - - lazy val scenarioActivityEndpoint: SecuredEndpoint[ProcessName, ScenarioActivityError, ScenarioActivity, Any] = - baseNuApiEndpoint - .summary("Scenario activity service") - .tag("Scenario") - .get - .in("processes" / path[ProcessName]("scenarioName") / "activity") - .out( - statusCode(Ok).and( - jsonBody[ScenarioActivity].example( - Example.of( - summary = Some("Display scenario activity"), - value = ScenarioActivity( - comments = List( - Comment( - id = 1L, - processVersionId = 1L, - content = "some comment", - user = "test", - createDate = Instant.parse("2024-01-17T14:21:17Z") - ) - ), - attachments = List( - Attachment( - id = 1L, - processVersionId = 1L, - fileName = "some_file.txt", - user = "test", - createDate = Instant.parse("2024-01-17T14:21:17Z") - ) - ) - ) - ) - ) - ) - ) - .errorOut(scenarioNotFoundErrorOutput) - .withSecurity(auth) - - lazy val addCommentEndpoint: SecuredEndpoint[AddCommentRequest, ScenarioActivityError, Unit, Any] = - baseNuApiEndpoint - .summary("Add scenario comment service") - .tag("Scenario") - .post - .in( - ("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" - / "comments" / stringBody).mapTo[AddCommentRequest] - ) - .out(statusCode(Ok)) - .errorOut(scenarioNotFoundErrorOutput) - .withSecurity(auth) - - lazy val deleteCommentEndpoint: SecuredEndpoint[DeleteCommentRequest, ScenarioActivityError, Unit, Any] = - baseNuApiEndpoint - .summary("Delete process comment service") - .tag("Scenario") - .delete - .in( - ("processes" / path[ProcessName]("scenarioName") / "activity" / "comments" - / path[Long]("commentId")).mapTo[DeleteCommentRequest] - ) - .out(statusCode(Ok)) - .errorOut( - oneOf[ScenarioActivityError]( - oneOfVariantFromMatchType( - NotFound, - plainBody[NoScenario] - .example( - Example.of( - summary = Some("No scenario {scenarioName} found"), - value = NoScenario(ProcessName("'example scenario'")) - ) - ) - ), - oneOfVariantFromMatchType( - InternalServerError, - plainBody[NoComment] - .example( - Example.of( - summary = Some("Unable to delete comment with id: {commentId}"), - value = NoComment(1L) - ) - ) - ) - ) - ) - .withSecurity(auth) - - def addAttachmentEndpoint( - implicit streamBodyEndpoint: EndpointInput[InputStream] - ): SecuredEndpoint[AddAttachmentRequest, ScenarioActivityError, Unit, Any] = { - baseNuApiEndpoint - .summary("Add scenario attachment service") - .tag("Scenario") - .post - .in( - ( - "processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" - / "attachments" / streamBodyEndpoint / header[FileName](HeaderNames.ContentDisposition) - ).mapTo[AddAttachmentRequest] - ) - .out(statusCode(Ok)) - .errorOut(scenarioNotFoundErrorOutput) - .withSecurity(auth) - } - - def downloadAttachmentEndpoint( - implicit streamBodyEndpoint: EndpointOutput[InputStream] - ): SecuredEndpoint[GetAttachmentRequest, ScenarioActivityError, GetAttachmentResponse, Any] = { - baseNuApiEndpoint - .summary("Download attachment service") - .tag("Scenario") - .get - .in( - ("processes" / path[ProcessName]("processName") / "activity" / "attachments" - / path[Long]("attachmentId")).mapTo[GetAttachmentRequest] - ) - .out( - statusCode(Ok) - .and(streamBodyEndpoint) - .and(header(HeaderNames.ContentDisposition)(optionalHeaderCodec)) - .and(header(HeaderNames.ContentType)(requiredHeaderCodec)) - .mapTo[GetAttachmentResponse] - ) - .errorOut(scenarioNotFoundErrorOutput) - .withSecurity(auth) - } - - private lazy val scenarioNotFoundErrorOutput: EndpointOutput.OneOf[ScenarioActivityError, ScenarioActivityError] = - oneOf[ScenarioActivityError]( - oneOfVariantFromMatchType( - NotFound, - plainBody[NoScenario] - .example( - Example.of( - summary = Some("No scenario {scenarioName} found"), - value = NoScenario(ProcessName("'example scenario'")) - ) - ) - ) - ) - -} - -object ScenarioActivityApiEndpoints { - - object Dtos { - @derive(encoder, decoder, schema) - final case class ScenarioActivity private (comments: List[Comment], attachments: List[Attachment]) - - object ScenarioActivity { - - def apply(activity: DbProcessActivity): ScenarioActivity = - new ScenarioActivity( - comments = activity.comments.map(Comment.apply), - attachments = activity.attachments.map(Attachment.apply) - ) - - } - - @derive(encoder, decoder, schema) - final case class Comment private ( - id: Long, - processVersionId: Long, - content: String, - user: String, - createDate: Instant - ) - - object Comment { - - def apply(comment: DbComment): Comment = - new Comment( - id = comment.id, - processVersionId = comment.processVersionId.value, - content = comment.content, - user = comment.user, - createDate = comment.createDate - ) - - } - - @derive(encoder, decoder, schema) - final case class Attachment private ( - id: Long, - processVersionId: Long, - fileName: String, - user: String, - createDate: Instant - ) - - object Attachment { - - def apply(attachment: DbAttachment): Attachment = - new Attachment( - id = attachment.id, - processVersionId = attachment.processVersionId.value, - fileName = attachment.fileName, - user = attachment.user, - createDate = attachment.createDate - ) - - } - - final case class AddCommentRequest(scenarioName: ProcessName, versionId: VersionId, commentContent: String) - - final case class DeleteCommentRequest(scenarioName: ProcessName, commentId: Long) - - final case class AddAttachmentRequest( - scenarioName: ProcessName, - versionId: VersionId, - body: InputStream, - fileName: FileName - ) - - final case class GetAttachmentRequest(scenarioName: ProcessName, attachmentId: Long) - - final case class GetAttachmentResponse(inputStream: InputStream, fileName: Option[String], contentType: String) - - object GetAttachmentResponse { - val emptyResponse: GetAttachmentResponse = - GetAttachmentResponse(InputStream.nullInputStream(), None, MediaType.TextPlainUtf8.toString()) - } - - sealed trait ScenarioActivityError - - object ScenarioActivityError { - final case class NoScenario(scenarioName: ProcessName) extends ScenarioActivityError - final case object NoPermission extends ScenarioActivityError with CustomAuthorizationError - 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" - ) - - implicit val noCommentCodec: Codec[String, NoComment, CodecFormat.TextPlain] = - BaseEndpointDefinitions.toTextPlainCodecSerializationOnly[NoComment](e => - s"Unable to delete comment with id: ${e.commentId}" - ) - - } - - } - -} 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 new file mode 100644 index 00000000000..bbae3da1bb6 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Dtos.scala @@ -0,0 +1,528 @@ +package pl.touk.nussknacker.ui.api.description.scenarioActivity + +import derevo.circe.{decoder, encoder} +import derevo.derive +import enumeratum.EnumEntry.UpperSnakecase +import enumeratum.{Enum, EnumEntry} +import io.circe +import io.circe.generic.extras +import io.circe.generic.extras.semiauto.deriveConfiguredCodec +import io.circe.{Decoder, Encoder} +import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} +import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions +import pl.touk.nussknacker.ui.api.BaseHttpService.CustomAuthorizationError +import pl.touk.nussknacker.ui.api.TapirCodecs.enumSchema +import pl.touk.nussknacker.ui.server.HeadersSupport.FileName +import sttp.model.MediaType +import sttp.tapir._ +import sttp.tapir.derevo.schema +import sttp.tapir.generic.Configuration + +import java.io.InputStream +import java.time.Instant +import java.util.UUID +import scala.collection.immutable + +object Dtos { + + @derive(encoder, decoder, schema) + final case class ScenarioActivitiesMetadata( + activities: List[ScenarioActivityMetadata], + actions: List[ScenarioActivityActionMetadata], + ) + + object ScenarioActivitiesMetadata { + + val default: ScenarioActivitiesMetadata = ScenarioActivitiesMetadata( + activities = ScenarioActivityType.values.map(ScenarioActivityMetadata.from).toList, + actions = List( + ScenarioActivityActionMetadata( + id = "compare", + displayableName = "Compare", + icon = "/assets/states/error.svg" + ), + ScenarioActivityActionMetadata( + id = "delete_comment", + displayableName = "Delete", + icon = "/assets/states/error.svg" + ), + ScenarioActivityActionMetadata( + id = "edit_comment", + displayableName = "Edit", + icon = "/assets/states/error.svg" + ), + ScenarioActivityActionMetadata( + id = "download_attachment", + displayableName = "Download", + icon = "/assets/states/error.svg" + ), + ScenarioActivityActionMetadata( + id = "delete_attachment", + displayableName = "Delete", + icon = "/assets/states/error.svg" + ), + ) + ) + + } + + @derive(encoder, decoder, schema) + final case class ScenarioActivityActionMetadata( + id: String, + displayableName: String, + icon: String, + ) + + @derive(encoder, decoder, schema) + final case class ScenarioActivityMetadata( + `type`: String, + displayableName: String, + icon: String, + supportedActions: List[String], + ) + + object ScenarioActivityMetadata { + + def from(scenarioActivityType: ScenarioActivityType): ScenarioActivityMetadata = + ScenarioActivityMetadata( + `type` = scenarioActivityType.entryName, + displayableName = scenarioActivityType.displayableName, + icon = scenarioActivityType.icon, + supportedActions = scenarioActivityType.supportedActions, + ) + + } + + sealed trait ScenarioActivityType extends EnumEntry with UpperSnakecase { + def displayableName: String + def icon: String + def supportedActions: List[String] + } + + object ScenarioActivityType extends Enum[ScenarioActivityType] { + + case object ScenarioCreated extends ScenarioActivityType { + override def displayableName: String = "Scenario created" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioArchived extends ScenarioActivityType { + override def displayableName: String = "Scenario archived" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioUnarchived extends ScenarioActivityType { + override def displayableName: String = "Scenario unarchived" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioDeployed extends ScenarioActivityType { + override def displayableName: String = "Deployment" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioPaused extends ScenarioActivityType { + override def displayableName: String = "Pause" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioCanceled extends ScenarioActivityType { + override def displayableName: String = "Cancel" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioModified extends ScenarioActivityType { + override def displayableName: String = "New version saved" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List("compare") + } + + case object ScenarioNameChanged extends ScenarioActivityType { + override def displayableName: String = "Scenario name changed" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object CommentAdded extends ScenarioActivityType { + override def displayableName: String = "Comment" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List("delete_comment", "edit_comment") + } + + case object AttachmentAdded extends ScenarioActivityType { + override def displayableName: String = "Attachment" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ChangedProcessingMode extends ScenarioActivityType { + override def displayableName: String = "Processing mode change" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object IncomingMigration extends ScenarioActivityType { + override def displayableName: String = "Incoming migration" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List("compare") + } + + case object OutgoingMigration extends ScenarioActivityType { + override def displayableName: String = "Outgoing migration" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object PerformedSingleExecution extends ScenarioActivityType { + override def displayableName: String = "Processing data" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object PerformedScheduledExecution extends ScenarioActivityType { + override def displayableName: String = "Processing data" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object AutomaticUpdate extends ScenarioActivityType { + override def displayableName: String = "Automatic update" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List("compare") + } + + case object CustomAction extends ScenarioActivityType { + override def displayableName: String = "Custom action" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + override def values: immutable.IndexedSeq[ScenarioActivityType] = findValues + + implicit def scenarioActivityTypeSchema: Schema[ScenarioActivityType] = + enumSchema[ScenarioActivityType]( + ScenarioActivityType.values.toList, + _.entryName, + ) + + implicit def scenarioActivityTypeCodec: circe.Codec[ScenarioActivityType] = circe.Codec.from( + Decoder.decodeString.emap(str => + ScenarioActivityType.withNameEither(str).left.map(_ => s"Invalid scenario action type [$str]") + ), + Encoder.encodeString.contramap(_.entryName), + ) + + implicit def scenarioActivityTypeTextCodec: Codec[String, ScenarioActivityType, CodecFormat.TextPlain] = + Codec.string.map( + Mapping.fromDecode[String, ScenarioActivityType] { + ScenarioActivityType.withNameOption(_) match { + case Some(value) => DecodeResult.Value(value) + case None => DecodeResult.InvalidValue(Nil) + } + }(_.entryName) + ) + + } + + @derive(encoder, decoder, schema) + final case class ScenarioActivityComment(comment: Option[String], lastModifiedBy: String, lastModifiedAt: Instant) + + @derive(encoder, decoder, schema) + final case class ScenarioActivityAttachment( + id: Option[Long], + filename: String, + lastModifiedBy: String, + lastModifiedAt: Instant + ) + + @derive(encoder, decoder, schema) + final case class ScenarioActivities(activities: List[ScenarioActivity]) + + sealed trait ScenarioActivity { + def id: UUID + def user: String + def date: Instant + def scenarioVersion: Option[Long] + } + + object ScenarioActivity { + + implicit def scenarioActivityCodec: circe.Codec[ScenarioActivity] = { + implicit val configuration: extras.Configuration = + extras.Configuration.default.withDiscriminator("type").withScreamingSnakeCaseConstructorNames + deriveConfiguredCodec + } + + implicit def scenarioActivitySchema: Schema[ScenarioActivity] = { + implicit val configuration: Configuration = + Configuration.default.withDiscriminator("type").withScreamingSnakeCaseDiscriminatorValues + Schema.derived[ScenarioActivity] + } + + final case class ScenarioCreated( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + ) extends ScenarioActivity + + final case class ScenarioArchived( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + ) extends ScenarioActivity + + final case class ScenarioUnarchived( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + ) extends ScenarioActivity + + // Scenario deployments + + final case class ScenarioDeployed( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + final case class ScenarioPaused( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + final case class ScenarioCanceled( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + // Scenario modifications + + final case class ScenarioModified( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + final case class ScenarioNameChanged( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + oldName: String, + newName: String, + ) extends ScenarioActivity + + final case class CommentAdded( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + final case class AttachmentAdded( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + attachment: ScenarioActivityAttachment, + ) extends ScenarioActivity + + final case class ChangedProcessingMode( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + from: String, + to: String, + ) extends ScenarioActivity + + // Migration between environments + + final case class IncomingMigration( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + sourceEnvironment: String, + sourceScenarioVersion: String, + ) extends ScenarioActivity + + final case class OutgoingMigration( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + destinationEnvironment: String, + ) extends ScenarioActivity + + // Batch + + final case class PerformedSingleExecution( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + dateFinished: Instant, + errorMessage: Option[String], + ) extends ScenarioActivity + + final case class PerformedScheduledExecution( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + dateFinished: Instant, + errorMessage: Option[String], + ) extends ScenarioActivity + + // Other/technical + + final case class AutomaticUpdate( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + dateFinished: Instant, + changes: String, + errorMessage: Option[String], + ) extends ScenarioActivity + + final case class CustomAction( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + actionName: String, + ) extends ScenarioActivity + + } + + @derive(encoder, decoder, schema) + final case class ScenarioAttachments(attachments: List[Attachment]) + + @derive(encoder, decoder, schema) + final case class Comment private ( + id: Long, + scenarioVersion: Long, + content: String, + user: String, + createDate: Instant + ) + + @derive(encoder, decoder, schema) + final case class Attachment private ( + id: Long, + scenarioVersion: Long, + fileName: String, + user: String, + createDate: Instant + ) + + final case class AddCommentRequest(scenarioName: ProcessName, versionId: VersionId, commentContent: String) + + final case class DeprecatedEditCommentRequest( + scenarioName: ProcessName, + commentId: Long, + commentContent: String + ) + + final case class EditCommentRequest( + scenarioName: ProcessName, + scenarioActivityId: UUID, + commentContent: String + ) + + final case class DeleteCommentRequest( + scenarioName: ProcessName, + scenarioActivityId: UUID + ) + + final case class DeprecatedDeleteCommentRequest( + scenarioName: ProcessName, + commentId: Long, + ) + + final case class AddAttachmentRequest( + scenarioName: ProcessName, + versionId: VersionId, + body: InputStream, + fileName: FileName + ) + + final case class GetAttachmentRequest(scenarioName: ProcessName, attachmentId: Long) + + final case class GetAttachmentResponse(inputStream: InputStream, fileName: Option[String], contentType: String) + + object GetAttachmentResponse { + val emptyResponse: GetAttachmentResponse = + GetAttachmentResponse(InputStream.nullInputStream(), None, MediaType.TextPlainUtf8.toString()) + } + + sealed trait ScenarioActivityError + + 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 + + implicit val noScenarioCodec: Codec[String, NoScenario, CodecFormat.TextPlain] = + BaseEndpointDefinitions.toTextPlainCodecSerializationOnly[NoScenario](e => s"No scenario ${e.scenarioName} found") + + implicit val noCommentCodec: Codec[String, NoComment, CodecFormat.TextPlain] = + BaseEndpointDefinitions.toTextPlainCodecSerializationOnly[NoComment](e => + s"Unable to delete comment with id: ${e.commentId}" + ) + + } + + object Legacy { + + @derive(encoder, decoder, schema) + final case class ProcessActivity private (comments: List[Comment], attachments: List[Attachment]) + + @derive(encoder, decoder, schema) + final case class Comment( + id: Long, + processVersionId: Long, + content: String, + user: String, + createDate: Instant + ) + + @derive(encoder, decoder, schema) + final case class Attachment( + id: Long, + processVersionId: Long, + fileName: String, + user: String, + createDate: Instant + ) + + } + +} 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 new file mode 100644 index 00000000000..a706ebd4dc4 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Endpoints.scala @@ -0,0 +1,192 @@ +package pl.touk.nussknacker.ui.api.description.scenarioActivity + +import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} +import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions +import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions.SecuredEndpoint +import pl.touk.nussknacker.security.AuthCredentials +import pl.touk.nussknacker.ui.api.TapirCodecs +import pl.touk.nussknacker.ui.server.HeadersSupport.FileName +import pl.touk.nussknacker.ui.server.TapirStreamEndpointProvider +import sttp.model.HeaderNames +import sttp.model.StatusCode.{InternalServerError, NotFound, Ok} +import sttp.tapir._ +import sttp.tapir.json.circe.jsonBody + +import java.util.UUID + +class Endpoints(auth: EndpointInput[AuthCredentials], streamProvider: TapirStreamEndpointProvider) + extends BaseEndpointDefinitions { + + import TapirCodecs.ContentDispositionCodec._ + import TapirCodecs.HeaderCodec._ + import TapirCodecs.ScenarioNameCodec._ + import TapirCodecs.VersionIdCodec._ + import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError._ + import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos._ + import pl.touk.nussknacker.ui.api.description.scenarioActivity.InputOutput._ + + lazy val deprecatedScenarioActivityEndpoint + : SecuredEndpoint[ProcessName, ScenarioActivityError, Legacy.ProcessActivity, Any] = + baseNuApiEndpoint + .summary("Scenario activity service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity") + .out(statusCode(Ok).and(jsonBody[Legacy.ProcessActivity].example(Examples.deprecatedScenarioActivity))) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + .deprecated() + + lazy val deprecatedAddCommentEndpoint: SecuredEndpoint[AddCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Add scenario comment service") + .tag("Activities") + .post + .in("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" / "comments") + .in(stringBody) + .mapInTo[AddCommentRequest] + .out(statusCode(Ok)) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + .deprecated() + + lazy val deprecatedDeleteCommentEndpoint + : SecuredEndpoint[DeprecatedDeleteCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Delete process comment service") + .tag("Activities") + .delete + .in( + "processes" / path[ProcessName]("scenarioName") / "activity" / "comments" / path[Long]("commentId") + ) + .mapInTo[DeprecatedDeleteCommentRequest] + .out(statusCode(Ok)) + .errorOut( + oneOf[ScenarioActivityError]( + oneOfVariantFromMatchType(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), + oneOfVariantFromMatchType(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)) + ) + ) + .withSecurity(auth) + .deprecated() + + lazy val scenarioActivitiesEndpoint: SecuredEndpoint[ + ProcessName, + ScenarioActivityError, + ScenarioActivities, + Any + ] = + baseNuApiEndpoint + .summary("Scenario activities service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity" / "activities") + .out(statusCode(Ok).and(jsonBody[ScenarioActivities].example(Examples.scenarioActivities))) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + + lazy val scenarioActivitiesMetadataEndpoint + : SecuredEndpoint[ProcessName, ScenarioActivityError, ScenarioActivitiesMetadata, Any] = + baseNuApiEndpoint + .summary("Scenario activities metadata service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity" / "activities" / "metadata") + .out(statusCode(Ok).and(jsonBody[ScenarioActivitiesMetadata].example(ScenarioActivitiesMetadata.default))) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + + lazy val addCommentEndpoint: SecuredEndpoint[AddCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Add scenario comment service") + .tag("Activities") + .post + .in("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" / "comment") + .in(stringBody) + .mapInTo[AddCommentRequest] + .out(statusCode(Ok)) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + + lazy val editCommentEndpoint: SecuredEndpoint[EditCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Edit process comment service") + .tag("Activities") + .put + .in( + "processes" / path[ProcessName]("scenarioName") / "activity" / "comment" / path[UUID]("scenarioActivityId") + ) + .in(stringBody) + .mapInTo[EditCommentRequest] + .out(statusCode(Ok)) + .errorOut( + oneOf[ScenarioActivityError]( + oneOfVariantFromMatchType(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), + oneOfVariantFromMatchType(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)) + ) + ) + .withSecurity(auth) + + lazy val deleteCommentEndpoint: SecuredEndpoint[DeleteCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Delete process comment service") + .tag("Activities") + .delete + .in( + "processes" / path[ProcessName]("scenarioName") / "activity" / "comment" / path[UUID]("scenarioActivityId") + ) + .mapInTo[DeleteCommentRequest] + .out(statusCode(Ok)) + .errorOut( + oneOf[ScenarioActivityError]( + oneOfVariantFromMatchType(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), + oneOfVariantFromMatchType(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)) + ) + ) + .withSecurity(auth) + + val attachmentsEndpoint: SecuredEndpoint[ProcessName, ScenarioActivityError, ScenarioAttachments, Any] = { + baseNuApiEndpoint + .summary("Scenario attachments service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity" / "attachments") + .out(statusCode(Ok).and(jsonBody[ScenarioAttachments].example(Examples.scenarioAttachments))) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + } + + val addAttachmentEndpoint: SecuredEndpoint[AddAttachmentRequest, ScenarioActivityError, Unit, Any] = { + baseNuApiEndpoint + .summary("Add scenario attachment service") + .tag("Activities") + .post + .in("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" / "attachments") + .in(streamProvider.streamBodyEndpointInput) + .in(header[FileName](HeaderNames.ContentDisposition)) + .mapInTo[AddAttachmentRequest] + .out(statusCode(Ok)) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + } + + val downloadAttachmentEndpoint + : SecuredEndpoint[GetAttachmentRequest, ScenarioActivityError, GetAttachmentResponse, Any] = { + baseNuApiEndpoint + .summary("Download attachment service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity" / "attachments" / path[Long]("attachmentId")) + .mapInTo[GetAttachmentRequest] + .out( + statusCode(Ok) + .and(streamProvider.streamBodyEndpointOutput) + .and(header(HeaderNames.ContentDisposition)(optionalHeaderCodec)) + .and(header(HeaderNames.ContentType)(requiredHeaderCodec)) + .mapTo[GetAttachmentResponse] + ) + .errorOut(scenarioNotFoundErrorOutput) + .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 new file mode 100644 index 00000000000..1050cd95c99 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Examples.scala @@ -0,0 +1,236 @@ +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._ +import sttp.tapir.EndpointIO.Example + +import java.time.Instant +import java.util.UUID + +object Examples { + + val deprecatedScenarioActivity: Example[Legacy.ProcessActivity] = Example.of( + summary = Some("Display scenario activity"), + value = Legacy.ProcessActivity( + comments = List( + Legacy.Comment( + id = 1L, + processVersionId = 1L, + content = "some comment", + user = "test", + createDate = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + attachments = List( + Legacy.Attachment( + id = 1L, + processVersionId = 1L, + fileName = "some_file.txt", + user = "test", + createDate = Instant.parse("2024-01-17T14:21:17Z") + ) + ) + ) + ) + + val scenarioActivities: Example[ScenarioActivities] = Example.of( + summary = Some("Display scenario actions"), + value = ScenarioActivities( + activities = List( + ScenarioActivity.ScenarioCreated( + id = UUID.fromString("80c95497-3b53-4435-b2d9-ae73c5766213"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + ), + ScenarioActivity.ScenarioArchived( + id = UUID.fromString("070a4e5c-21e5-4e63-acac-0052cf705a90"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + ), + ScenarioActivity.ScenarioUnarchived( + id = UUID.fromString("fa35d944-fe20-4c4f-96c6-316b6197951a"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + ), + ScenarioActivity.ScenarioDeployed( + id = UUID.fromString("545b7d87-8cdf-4cb5-92c4-38ddbfca3d08"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Deployment of scenario - task JIRA-1234"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + ScenarioActivity.ScenarioCanceled( + id = UUID.fromString("c354eba1-de97-455c-b977-74729c41ce7"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Canceled because marketing campaign ended"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + ScenarioActivity.ScenarioModified( + id = UUID.fromString("07b04d45-c7c0-4980-a3bc-3c7f66410f68"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Added new processing step"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + ScenarioActivity.ScenarioNameChanged( + id = UUID.fromString("da3d1f78-7d73-4ed9-b0e5-95538e150d0d"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + oldName = "marketing campaign", + newName = "old marketing campaign", + ), + ScenarioActivity.CommentAdded( + id = UUID.fromString("edf8b047-9165-445d-a173-ba61812dbd63"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Added new processing step"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + ScenarioActivity.CommentAdded( + id = UUID.fromString("369367d6-d445-4327-ac23-4a94367b1d9e"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = None, + lastModifiedBy = "John Doe", + lastModifiedAt = Instant.parse("2024-01-18T14:21:17Z") + ) + ), + ScenarioActivity.AttachmentAdded( + id = UUID.fromString("b29916a9-34d4-4fc2-a6ab-79569f68c0b2"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + attachment = ScenarioActivityAttachment( + id = Some(10000001), + filename = "attachment01.png", + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ), + ), + ScenarioActivity.AttachmentAdded( + id = UUID.fromString("d0a7f4a2-abcc-4ffa-b1ca-68f6da3e999a"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + attachment = ScenarioActivityAttachment( + id = None, + filename = "attachment01.png", + lastModifiedBy = "John Doe", + lastModifiedAt = Instant.parse("2024-01-18T14:21:17Z") + ), + ), + ScenarioActivity.ChangedProcessingMode( + id = UUID.fromString("683df470-0b33-4ead-bf61-fa35c63484f3"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + from = "Request-Response", + to = "Batch", + ), + ScenarioActivity.IncomingMigration( + id = UUID.fromString("4da0f1ac-034a-49b6-81c9-8ee48ba1d830"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + sourceEnvironment = "preprod", + sourceScenarioVersion = "23", + ), + ScenarioActivity.OutgoingMigration( + id = UUID.fromString("49fcd45d-3fa6-48d4-b8ed-b3055910c7ad"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Added new processing step"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ), + destinationEnvironment = "preprod", + ), + ScenarioActivity.PerformedSingleExecution( + id = UUID.fromString("924dfcd3-fbc7-44ea-8763-813874382204"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + errorMessage = Some("Execution error occurred"), + ), + ScenarioActivity.PerformedSingleExecution( + id = UUID.fromString("924dfcd3-fbc7-44ea-8763-813874382204"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + errorMessage = None, + ), + ScenarioActivity.PerformedScheduledExecution( + id = UUID.fromString("9b27797e-aa03-42ba-8406-d0ae8005a883"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + errorMessage = None, + ), + ScenarioActivity.AutomaticUpdate( + id = UUID.fromString("33509d37-7657-4229-940f-b5736c82fb13"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + changes = "JIRA-12345, JIRA-32146", + errorMessage = None, + ), + ), + ) + ) + + val scenarioAttachments: Example[ScenarioAttachments] = Example.of( + summary = Some("Display scenario activity"), + value = ScenarioAttachments( + attachments = List( + Attachment( + id = 1L, + scenarioVersion = 1L, + fileName = "some_file.txt", + user = "test", + createDate = Instant.parse("2024-01-17T14:21:17Z") + ) + ) + ) + ) + + val noScenarioError: Example[NoScenario] = Example.of( + summary = Some("No scenario {scenarioName} found"), + value = NoScenario(ProcessName("'example scenario'")) + ) + + val commentNotFoundError: Example[NoComment] = Example.of( + summary = Some("Unable to edit comment with id: {commentId}"), + value = NoComment("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 new file mode 100644 index 00000000000..701f74bab3e --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/InputOutput.scala @@ -0,0 +1,30 @@ +package pl.touk.nussknacker.ui.api.description.scenarioActivity + +import pl.touk.nussknacker.engine.api.process.ProcessName +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError.NoScenario +import sttp.model.StatusCode.{NotFound, NotImplemented} +import sttp.tapir.EndpointIO.Example +import sttp.tapir.{EndpointOutput, emptyOutputAs, oneOf, oneOfVariantFromMatchType, plainBody} + +object InputOutput { + + val scenarioNotFoundErrorOutput: EndpointOutput.OneOf[ScenarioActivityError, ScenarioActivityError] = + oneOf[ScenarioActivityError]( + oneOfVariantFromMatchType( + NotFound, + plainBody[NoScenario] + .example( + Example.of( + summary = Some("No scenario {scenarioName} found"), + value = NoScenario(ProcessName("'example scenario'")) + ) + ) + ), + oneOfVariantFromMatchType( + NotImplemented, + emptyOutputAs(ScenarioActivityError.NotImplemented), + ) + ) + +} diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala index 49ade0644d3..7c0bc88ff69 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala @@ -1,5 +1,7 @@ package pl.touk.nussknacker.ui.api +import akka.actor.ActorSystem +import akka.stream.Materializer import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers import org.scalatest.prop.TableDrivenPropertyChecks._ @@ -8,6 +10,7 @@ import pl.touk.nussknacker.security.AuthCredentials.PassedAuthCredentials import pl.touk.nussknacker.test.utils.domain.ReflectionBasedUtils import pl.touk.nussknacker.test.utils.{InvalidExample, OpenAPIExamplesValidator, OpenAPISchemaComponents} import pl.touk.nussknacker.ui.security.api.AuthManager.ImpersonationConsideringInputEndpoint +import pl.touk.nussknacker.ui.server.{AkkaHttpBasedTapirStreamEndpointProvider, TapirStreamEndpointProvider} import pl.touk.nussknacker.ui.services.NuDesignerExposedApiHttpService import pl.touk.nussknacker.ui.util.Project import sttp.apispec.openapi.circe.yaml.RichOpenAPI @@ -15,6 +18,7 @@ import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter import sttp.tapir.{Endpoint, EndpointInput, auth} import java.lang.reflect.{Method, Modifier} +import scala.concurrent.Await import scala.util.Try // if the test fails it probably means that you should regenerate the Nu Designer OpenAPI document @@ -138,30 +142,46 @@ class NuDesignerApiAvailableToExposeYamlSpec extends AnyFunSuite with Matchers { object NuDesignerApiAvailableToExpose { - def generateOpenApiYaml: String = { - val endpoints = findApiEndpointsClasses().flatMap(findEndpointsInClass) + def generateOpenApiYaml: String = withStreamProvider { streamProvider => + val endpoints = findApiEndpointsClasses().flatMap(findEndpointsInClass(streamProvider)) val docs = OpenAPIDocsInterpreter(NuDesignerExposedApiHttpService.openAPIDocsOptions).toOpenAPI( es = endpoints, title = NuDesignerExposedApiHttpService.openApiDocumentTitle, version = "" ) - docs.toYaml } + private def withStreamProvider[T](handle: TapirStreamEndpointProvider => T): T = { + val actorSystem: ActorSystem = ActorSystem() + val mat: Materializer = Materializer(actorSystem) + val streamProvider: TapirStreamEndpointProvider = new AkkaHttpBasedTapirStreamEndpointProvider()(mat) + val result = handle(streamProvider) + Await.result(actorSystem.terminate(), scala.concurrent.duration.Duration.Inf) + result + } + private def findApiEndpointsClasses() = { ReflectionBasedUtils.findSubclassesOf[BaseEndpointDefinitions]("pl.touk.nussknacker.ui.api") } - private def findEndpointsInClass(clazz: Class[_ <: BaseEndpointDefinitions]) = { - val endpointDefinitions = createInstanceOf(clazz) + private def findEndpointsInClass( + streamEndpointProvider: TapirStreamEndpointProvider + )( + clazz: Class[_ <: BaseEndpointDefinitions] + ) = { + val endpointDefinitions = createInstanceOf(streamEndpointProvider)(clazz) clazz.getDeclaredMethods.toList .filter(isEndpointMethod) .sortBy(_.getName) .map(instantiateEndpointDefinition(endpointDefinitions, _)) } - private def createInstanceOf(clazz: Class[_ <: BaseEndpointDefinitions]) = { + private def createInstanceOf( + streamEndpointProvider: TapirStreamEndpointProvider, + )( + clazz: Class[_ <: BaseEndpointDefinitions], + ) = { val basicAuth = auth .basic[Option[String]]() .map(_.map(PassedAuthCredentials))(_.map(_.value)) @@ -173,6 +193,10 @@ object NuDesignerApiAvailableToExpose { Try(clazz.getDeclaredConstructor()) .map(_.newInstance()) } + .orElse { + Try(clazz.getConstructor(classOf[EndpointInput[PassedAuthCredentials]], classOf[TapirStreamEndpointProvider])) + .map(_.newInstance(basicAuth, streamEndpointProvider)) + } .getOrElse( throw new IllegalStateException( s"Class ${clazz.getName} is required to have either one parameter constructor or constructor without parameters" diff --git a/docs-internal/api/nu-designer-openapi.yaml b/docs-internal/api/nu-designer-openapi.yaml index 5e78df23cba..eeda77e9167 100644 --- a/docs-internal/api/nu-designer-openapi.yaml +++ b/docs-internal/api/nu-designer-openapi.yaml @@ -2249,12 +2249,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/{versionId}/activity/comments: - post: + /api/scenarioParametersCombinations: + get: tags: - - Scenario - summary: Add scenario comment service - operationId: postApiProcessesScenarionameVersionidActivityComments + - App + summary: Service providing available combinations of scenario's parameters + operationId: getApiScenarioparameterscombinations parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2263,29 +2263,32 @@ paths: type: - string - 'null' - - name: scenarioName - in: path - required: true - schema: - type: string - - name: versionId - in: path - required: true - schema: - type: integer - format: int64 - requestBody: - content: - text/plain: - schema: - type: string - required: true responses: '200': description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScenarioParametersCombinationWithEngineErrors' + examples: + Example: + summary: List of available parameters combinations + value: + combinations: + - processingMode: Unbounded-Stream + category: Marketing + engineSetupName: Flink + - processingMode: Request-Response + category: Fraud + engineSetupName: Lite K8s + - processingMode: Unbounded-Stream + category: Fraud + engineSetupName: Flink Fraud Detection + engineSetupErrors: + Flink: + - Invalid Flink configuration '400': - description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter versionId, Invalid value for: body' + description: 'Invalid value for: header Nu-Impersonate-User-Identity' content: text/plain: schema: @@ -2320,8 +2323,8 @@ paths: type: string examples: Example: - summary: No scenario {scenarioName} found - value: No scenario 'example scenario' found + summary: No impersonated user's data found for provided identity + value: No impersonated user data found for provided identity '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2336,12 +2339,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/comments/{commentId}: - delete: + /api/statistic: + post: tags: - - Scenario - summary: Delete process comment service - operationId: deleteApiProcessesScenarionameActivityCommentsCommentid + - Statistics + summary: Register statistics service + operationId: postApiStatistic parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2350,23 +2353,18 @@ paths: type: - string - 'null' - - name: scenarioName - in: path - required: true - schema: - type: string - - name: commentId - in: path + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterStatisticsRequestDto' required: true - schema: - type: integer - format: int64 responses: - '200': + '204': description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter commentId' + value for: body' content: text/plain: schema: @@ -2401,18 +2399,8 @@ paths: type: string examples: Example: - summary: No scenario {scenarioName} found - value: No scenario 'example scenario' found - '500': - description: '' - content: - text/plain: - schema: - type: string - examples: - Example: - summary: 'Unable to delete comment with id: {commentId}' - value: 'Unable to delete comment with id: 1' + summary: No impersonated user's data found for provided identity + value: No impersonated user data found for provided identity '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2427,12 +2415,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity: + /api/statistic/usage: get: tags: - - Scenario - summary: Scenario activity service - operationId: getApiProcessesScenarionameActivity + - Statistics + summary: Statistics URL service + operationId: getApiStatisticUsage parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2441,34 +2429,19 @@ paths: type: - string - 'null' - - name: scenarioName - in: path - required: true - schema: - type: string responses: '200': description: '' content: application/json: schema: - $ref: '#/components/schemas/ScenarioActivity' + $ref: '#/components/schemas/StatisticUrlResponseDto' examples: Example: - summary: Display scenario activity + summary: List of statistics URLs value: - comments: - - id: 1 - processVersionId: 1 - content: some comment - user: test - createDate: '2024-01-17T14:21:17Z' - attachments: - - id: 1 - processVersionId: 1 - fileName: some_file.txt - user: test - createDate: '2024-01-17T14:21:17Z' + urls: + - https://stats.nussknacker.io/?a_n=1&a_t=0&a_v=0&c=3&c_n=82&c_t=0&c_v=0&f_m=0&f_v=0&fingerprint=development&n_m=2&n_ma=0&n_mi=2&n_v=1&s_a=0&s_dm_c=1&s_dm_e=1&s_dm_f=2&s_dm_l=0&s_f=1&s_pm_b=0&s_pm_rr=1&s_pm_s=3&s_s=3&source=sources&u_ma=0&u_mi=0&u_v=0&v_m=2&v_ma=1&v_mi=3&v_v=2&version=1.15.0-SNAPSHOT '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity' content: @@ -2505,8 +2478,18 @@ paths: type: string examples: Example: - summary: No scenario {scenarioName} found - value: No scenario 'example scenario' found + summary: No impersonated user's data found for provided identity + value: No impersonated user data found for provided identity + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Statistics generation failed. + value: Statistics generation failed. '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2521,12 +2504,12 @@ paths: security: - {} - httpAuth: [] - /api/scenarioParametersCombinations: + /api/user: get: tags: - - App - summary: Service providing available combinations of scenario's parameters - operationId: getApiScenarioparameterscombinations + - User + summary: Logged user info service + operationId: getApiUser parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2541,24 +2524,31 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ScenarioParametersCombinationWithEngineErrors' + $ref: '#/components/schemas/DisplayableUser' examples: - Example: - summary: List of available parameters combinations + Example0: + summary: Common user info value: - combinations: - - processingMode: Unbounded-Stream - category: Marketing - engineSetupName: Flink - - processingMode: Request-Response - category: Fraud - engineSetupName: Lite K8s - - processingMode: Unbounded-Stream - category: Fraud - engineSetupName: Flink Fraud Detection - engineSetupErrors: - Flink: - - Invalid Flink configuration + id: reader + username: reader + isAdmin: false + categories: + - Category1 + categoryPermissions: + Category1: + - Read + globalPermissions: [] + Example1: + summary: Admin user info + value: + id: admin + username: admin + isAdmin: true + categories: + - Category1 + - Category2 + categoryPermissions: {} + globalPermissions: [] '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity' content: @@ -2611,12 +2601,12 @@ paths: security: - {} - httpAuth: [] - /api/statistic: + /api/processes/{scenarioName}/{versionId}/activity/attachments: post: tags: - - Statistics - summary: Register statistics service - operationId: postApiStatistic + - Activities + summary: Add scenario attachment service + operationId: postApiProcessesScenarionameVersionidActivityAttachments parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2625,18 +2615,36 @@ paths: type: - string - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: versionId + in: path + required: true + schema: + type: integer + format: int64 + - name: Content-Disposition + in: header + required: true + schema: + type: string requestBody: content: - application/json: + application/octet-stream: schema: - $ref: '#/components/schemas/RegisterStatisticsRequestDto' + type: string + format: binary required: true responses: - '204': + '200': description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: body' + value for: path parameter versionId, Invalid value for: body, Invalid + value for: header Content-Disposition' content: text/plain: schema: @@ -2671,8 +2679,8 @@ paths: type: string examples: Example: - summary: No impersonated user's data found for provided identity - value: No impersonated user data found for provided identity + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2687,12 +2695,12 @@ paths: security: - {} - httpAuth: [] - /api/statistic/usage: - get: + /api/processes/{scenarioName}/{versionId}/activity/comment: + post: tags: - - Statistics - summary: Statistics URL service - operationId: getApiStatisticUsage + - Activities + summary: Add scenario comment service + operationId: postApiProcessesScenarionameVersionidActivityComment parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2701,21 +2709,29 @@ paths: type: - string - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: versionId + in: path + required: true + schema: + type: integer + format: int64 + requestBody: + content: + text/plain: + schema: + type: string + required: true responses: '200': description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/StatisticUrlResponseDto' - examples: - Example: - summary: List of statistics URLs - value: - urls: - - https://stats.nussknacker.io/?a_n=1&a_t=0&a_v=0&c=3&c_n=82&c_t=0&c_v=0&f_m=0&f_v=0&fingerprint=development&n_m=2&n_ma=0&n_mi=2&n_v=1&s_a=0&s_dm_c=1&s_dm_e=1&s_dm_f=2&s_dm_l=0&s_f=1&s_pm_b=0&s_pm_rr=1&s_pm_s=3&s_s=3&source=sources&u_ma=0&u_mi=0&u_v=0&v_m=2&v_ma=1&v_mi=3&v_v=2&version=1.15.0-SNAPSHOT '400': - description: 'Invalid value for: header Nu-Impersonate-User-Identity' + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter versionId, Invalid value for: body' content: text/plain: schema: @@ -2750,18 +2766,873 @@ paths: type: string examples: Example: - summary: No impersonated user's data found for provided identity - value: No impersonated user data found for provided identity - '500': - description: '' - content: - text/plain: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/attachments: + get: + tags: + - Activities + summary: Scenario attachments service + operationId: getApiProcessesScenarionameActivityAttachments + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScenarioAttachments' + examples: + Example: + summary: Display scenario activity + value: + attachments: + - id: 1 + scenarioVersion: 1 + fileName: some_file.txt + user: test + createDate: '2024-01-17T14:21:17Z' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/comment/{scenarioActivityId}: + put: + tags: + - Activities + summary: Edit process comment service + operationId: putApiProcessesScenarionameActivityCommentScenarioactivityid + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: scenarioActivityId + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + text/plain: + schema: + type: string + required: true + responses: + '200': + description: '' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter scenarioActivityId, Invalid value for: body' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + delete: + tags: + - Activities + summary: Delete process comment service + operationId: deleteApiProcessesScenarionameActivityCommentScenarioactivityid + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: scenarioActivityId + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: '' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter scenarioActivityId' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/{versionId}/activity/comments: + post: + tags: + - Activities + summary: Add scenario comment service + operationId: postApiProcessesScenarionameVersionidActivityComments + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: versionId + in: path + required: true + schema: + type: integer + format: int64 + requestBody: + content: + text/plain: + schema: + type: string + required: true + responses: + '200': + description: '' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter versionId, Invalid value for: body' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + deprecated: true + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/comments/{commentId}: + delete: + tags: + - Activities + summary: Delete process comment service + operationId: deleteApiProcessesScenarionameActivityCommentsCommentid + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: commentId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: '' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter commentId' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + deprecated: true + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity: + get: + tags: + - Activities + summary: Scenario activity service + operationId: getApiProcessesScenarionameActivity + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessActivity' + examples: + Example: + summary: Display scenario activity + value: + comments: + - id: 1 + processVersionId: 1 + content: some comment + user: test + createDate: '2024-01-17T14:21:17Z' + attachments: + - id: 1 + processVersionId: 1 + fileName: some_file.txt + user: test + createDate: '2024-01-17T14:21:17Z' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + deprecated: true + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/attachments/{attachmentId}: + get: + tags: + - Activities + summary: Download attachment service + operationId: getApiProcessesScenarionameActivityAttachmentsAttachmentid + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: attachmentId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: '' + headers: + Content-Disposition: + required: false + schema: + type: + - string + - 'null' + Content-Type: + required: true + schema: + type: string + content: + application/octet-stream: + schema: + type: string + format: binary + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter attachmentId' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/activities: + get: + tags: + - Activities + summary: Scenario activities service + operationId: getApiProcessesScenarionameActivityActivities + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScenarioActivities' + examples: + Example: + summary: Display scenario actions + value: + activities: + - id: 80c95497-3b53-4435-b2d9-ae73c5766213 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + type: SCENARIO_CREATED + - id: 070a4e5c-21e5-4e63-acac-0052cf705a90 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + type: SCENARIO_ARCHIVED + - id: fa35d944-fe20-4c4f-96c6-316b6197951a + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + type: SCENARIO_UNARCHIVED + - id: 545b7d87-8cdf-4cb5-92c4-38ddbfca3d08 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Deployment of scenario - task JIRA-1234 + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: SCENARIO_DEPLOYED + - id: c354eba1-de97-455c-b977-074729c41ce7 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Canceled because marketing campaign ended + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: SCENARIO_CANCELED + - id: 07b04d45-c7c0-4980-a3bc-3c7f66410f68 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Added new processing step + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: SCENARIO_MODIFIED + - id: da3d1f78-7d73-4ed9-b0e5-95538e150d0d + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + oldName: marketing campaign + newName: old marketing campaign + type: SCENARIO_NAME_CHANGED + - id: edf8b047-9165-445d-a173-ba61812dbd63 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Added new processing step + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: COMMENT_ADDED + - id: 369367d6-d445-4327-ac23-4a94367b1d9e + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + lastModifiedBy: John Doe + lastModifiedAt: '2024-01-18T14:21:17Z' + type: COMMENT_ADDED + - id: b29916a9-34d4-4fc2-a6ab-79569f68c0b2 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + attachment: + id: 10000001 + filename: attachment01.png + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: ATTACHMENT_ADDED + - id: d0a7f4a2-abcc-4ffa-b1ca-68f6da3e999a + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + attachment: + filename: attachment01.png + lastModifiedBy: John Doe + lastModifiedAt: '2024-01-18T14:21:17Z' + type: ATTACHMENT_ADDED + - id: 683df470-0b33-4ead-bf61-fa35c63484f3 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + from: Request-Response + to: Batch + type: CHANGED_PROCESSING_MODE + - id: 4da0f1ac-034a-49b6-81c9-8ee48ba1d830 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + sourceEnvironment: preprod + sourceScenarioVersion: '23' + type: INCOMING_MIGRATION + - id: 49fcd45d-3fa6-48d4-b8ed-b3055910c7ad + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Added new processing step + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + destinationEnvironment: preprod + type: OUTGOING_MIGRATION + - id: 924dfcd3-fbc7-44ea-8763-813874382204 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + dateFinished: '2024-01-17T14:21:17Z' + errorMessage: Execution error occurred + type: PERFORMED_SINGLE_EXECUTION + - id: 924dfcd3-fbc7-44ea-8763-813874382204 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + dateFinished: '2024-01-17T14:21:17Z' + type: PERFORMED_SINGLE_EXECUTION + - id: 9b27797e-aa03-42ba-8406-d0ae8005a883 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + dateFinished: '2024-01-17T14:21:17Z' + type: PERFORMED_SCHEDULED_EXECUTION + - id: 33509d37-7657-4229-940f-b5736c82fb13 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + dateFinished: '2024-01-17T14:21:17Z' + changes: JIRA-12345, JIRA-32146 + type: AUTOMATIC_UPDATE + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: schema: type: string examples: Example: - summary: Statistics generation failed. - value: Statistics generation failed. + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2776,12 +3647,12 @@ paths: security: - {} - httpAuth: [] - /api/user: + /api/processes/{scenarioName}/activity/activities/metadata: get: tags: - - User - summary: Logged user info service - operationId: getApiUser + - Activities + summary: Scenario activities metadata service + operationId: getApiProcessesScenarionameActivityActivitiesMetadata parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2790,37 +3661,109 @@ paths: type: - string - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string responses: '200': description: '' content: application/json: schema: - $ref: '#/components/schemas/DisplayableUser' - examples: - Example0: - summary: Common user info - value: - id: reader - username: reader - isAdmin: false - categories: - - Category1 - categoryPermissions: - Category1: - - Read - globalPermissions: [] - Example1: - summary: Admin user info - value: - id: admin - username: admin - isAdmin: true - categories: - - Category1 - - Category2 - categoryPermissions: {} - globalPermissions: [] + $ref: '#/components/schemas/ScenarioActivitiesMetadata' + example: + activities: + - type: SCENARIO_CREATED + displayableName: Scenario created + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_ARCHIVED + displayableName: Scenario archived + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_UNARCHIVED + displayableName: Scenario unarchived + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_DEPLOYED + displayableName: Deployment + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_PAUSED + displayableName: Pause + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_CANCELED + displayableName: Cancel + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_MODIFIED + displayableName: New version saved + icon: /assets/states/error.svg + supportedActions: + - compare + - type: SCENARIO_NAME_CHANGED + displayableName: Scenario name changed + icon: /assets/states/error.svg + supportedActions: [] + - type: COMMENT_ADDED + displayableName: Comment + icon: /assets/states/error.svg + supportedActions: + - delete_comment + - edit_comment + - type: ATTACHMENT_ADDED + displayableName: Attachment + icon: /assets/states/error.svg + supportedActions: [] + - type: CHANGED_PROCESSING_MODE + displayableName: Processing mode change + icon: /assets/states/error.svg + supportedActions: [] + - type: INCOMING_MIGRATION + displayableName: Incoming migration + icon: /assets/states/error.svg + supportedActions: + - compare + - type: OUTGOING_MIGRATION + displayableName: Outgoing migration + icon: /assets/states/error.svg + supportedActions: [] + - type: PERFORMED_SINGLE_EXECUTION + displayableName: Processing data + icon: /assets/states/error.svg + supportedActions: [] + - type: PERFORMED_SCHEDULED_EXECUTION + displayableName: Processing data + icon: /assets/states/error.svg + supportedActions: [] + - type: AUTOMATIC_UPDATE + displayableName: Automatic update + icon: /assets/states/error.svg + supportedActions: + - compare + - type: CUSTOM_ACTION + displayableName: Custom action + icon: /assets/states/error.svg + supportedActions: [] + actions: + - id: compare + displayableName: Compare + icon: /assets/states/error.svg + - id: delete_comment + displayableName: Delete + icon: /assets/states/error.svg + - id: edit_comment + displayableName: Edit + icon: /assets/states/error.svg + - id: download_attachment + displayableName: Download + icon: /assets/states/error.svg + - id: delete_attachment + displayableName: Delete + icon: /assets/states/error.svg '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity' content: @@ -2857,8 +3800,8 @@ paths: type: string examples: Example: - summary: No impersonated user's data found for provided identity - value: No impersonated user data found for provided identity + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2889,6 +3832,29 @@ components: type: integer format: int32 Attachment: + title: Attachment + type: object + required: + - id + - scenarioVersion + - fileName + - user + - createDate + properties: + id: + type: integer + format: int64 + scenarioVersion: + type: integer + format: int64 + fileName: + type: string + user: + type: string + createDate: + type: string + format: date-time + Attachment1: title: Attachment type: object required: @@ -2911,6 +3877,68 @@ components: createDate: type: string format: date-time + AttachmentAdded: + title: AttachmentAdded + type: object + required: + - id + - user + - date + - attachment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + attachment: + $ref: '#/components/schemas/ScenarioActivityAttachment' + type: + type: string + AutomaticUpdate: + title: AutomaticUpdate + type: object + required: + - id + - user + - date + - dateFinished + - changes + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + dateFinished: + type: string + format: date-time + changes: + type: string + errorMessage: + type: + - string + - 'null' + type: + type: string BoolParameterEditor: title: BoolParameterEditor type: object @@ -2989,6 +4017,36 @@ components: format: int32 errorMessage: type: string + ChangedProcessingMode: + title: ChangedProcessingMode + type: object + required: + - id + - user + - date + - from + - to + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + from: + type: string + to: + type: string + type: + type: string ColumnDefinition: title: ColumnDefinition type: object @@ -3023,6 +4081,33 @@ components: createDate: type: string format: date-time + CommentAdded: + title: CommentAdded + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string ComponentLink: title: ComponentLink type: object @@ -3130,6 +4215,33 @@ components: CronParameterEditor: title: CronParameterEditor type: object + CustomAction: + title: CustomAction + type: object + required: + - id + - user + - date + - actionName + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + actionName: + type: string + type: + type: string CustomActionRequest: title: CustomActionRequest type: object @@ -3545,6 +4657,36 @@ components: uniqueItems: true items: type: string + IncomingMigration: + title: IncomingMigration + type: object + required: + - id + - user + - date + - sourceEnvironment + - sourceScenarioVersion + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + sourceEnvironment: + type: string + sourceScenarioVersion: + type: string + type: + type: string JsonParameterEditor: title: JsonParameterEditor type: object @@ -4186,6 +5328,36 @@ components: - info - success - error + OutgoingMigration: + title: OutgoingMigration + type: object + required: + - id + - user + - date + - comment + - destinationEnvironment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + destinationEnvironment: + type: string + type: + type: string Parameter: title: Parameter type: object @@ -4270,14 +5442,78 @@ components: title: ParametersValidationResultDto type: object required: - - validationPerformed + - validationPerformed + properties: + validationErrors: + type: array + items: + $ref: '#/components/schemas/NodeValidationError' + validationPerformed: + type: boolean + PerformedScheduledExecution: + title: PerformedScheduledExecution + type: object + required: + - id + - user + - date + - dateFinished + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + dateFinished: + type: string + format: date-time + errorMessage: + type: + - string + - 'null' + type: + type: string + PerformedSingleExecution: + title: PerformedSingleExecution + type: object + required: + - id + - user + - date + - dateFinished + - type properties: - validationErrors: - type: array - items: - $ref: '#/components/schemas/NodeValidationError' - validationPerformed: - type: boolean + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + dateFinished: + type: string + format: date-time + errorMessage: + type: + - string + - 'null' + type: + type: string PeriodParameterEditor: title: PeriodParameterEditor type: object @@ -4352,6 +5588,18 @@ components: - FINISHED - FAILED - EXECUTION_FINISHED + ProcessActivity: + title: ProcessActivity + type: object + properties: + comments: + type: array + items: + $ref: '#/components/schemas/Comment' + attachments: + type: array + items: + $ref: '#/components/schemas/Attachment1' ProcessAdditionalFields: title: ProcessAdditionalFields type: object @@ -4415,18 +5663,301 @@ components: type: - string - 'null' + ScenarioActivities: + title: ScenarioActivities + type: object + properties: + activities: + type: array + items: + $ref: '#/components/schemas/ScenarioActivity' + ScenarioActivitiesMetadata: + title: ScenarioActivitiesMetadata + type: object + properties: + activities: + type: array + items: + $ref: '#/components/schemas/ScenarioActivityMetadata' + actions: + type: array + items: + $ref: '#/components/schemas/ScenarioActivityActionMetadata' ScenarioActivity: title: ScenarioActivity + oneOf: + - $ref: '#/components/schemas/AttachmentAdded' + - $ref: '#/components/schemas/AutomaticUpdate' + - $ref: '#/components/schemas/ChangedProcessingMode' + - $ref: '#/components/schemas/CommentAdded' + - $ref: '#/components/schemas/CustomAction' + - $ref: '#/components/schemas/IncomingMigration' + - $ref: '#/components/schemas/OutgoingMigration' + - $ref: '#/components/schemas/PerformedScheduledExecution' + - $ref: '#/components/schemas/PerformedSingleExecution' + - $ref: '#/components/schemas/ScenarioArchived' + - $ref: '#/components/schemas/ScenarioCanceled' + - $ref: '#/components/schemas/ScenarioCreated' + - $ref: '#/components/schemas/ScenarioDeployed' + - $ref: '#/components/schemas/ScenarioModified' + - $ref: '#/components/schemas/ScenarioNameChanged' + - $ref: '#/components/schemas/ScenarioPaused' + - $ref: '#/components/schemas/ScenarioUnarchived' + discriminator: + propertyName: type + mapping: + ATTACHMENT_ADDED: '#/components/schemas/AttachmentAdded' + AUTOMATIC_UPDATE: '#/components/schemas/AutomaticUpdate' + CHANGED_PROCESSING_MODE: '#/components/schemas/ChangedProcessingMode' + COMMENT_ADDED: '#/components/schemas/CommentAdded' + CUSTOM_ACTION: '#/components/schemas/CustomAction' + INCOMING_MIGRATION: '#/components/schemas/IncomingMigration' + OUTGOING_MIGRATION: '#/components/schemas/OutgoingMigration' + PERFORMED_SCHEDULED_EXECUTION: '#/components/schemas/PerformedScheduledExecution' + PERFORMED_SINGLE_EXECUTION: '#/components/schemas/PerformedSingleExecution' + SCENARIO_ARCHIVED: '#/components/schemas/ScenarioArchived' + SCENARIO_CANCELED: '#/components/schemas/ScenarioCanceled' + SCENARIO_CREATED: '#/components/schemas/ScenarioCreated' + SCENARIO_DEPLOYED: '#/components/schemas/ScenarioDeployed' + SCENARIO_MODIFIED: '#/components/schemas/ScenarioModified' + SCENARIO_NAME_CHANGED: '#/components/schemas/ScenarioNameChanged' + SCENARIO_PAUSED: '#/components/schemas/ScenarioPaused' + SCENARIO_UNARCHIVED: '#/components/schemas/ScenarioUnarchived' + ScenarioActivityActionMetadata: + title: ScenarioActivityActionMetadata type: object + required: + - id + - displayableName + - icon properties: - comments: + id: + type: string + displayableName: + type: string + icon: + type: string + ScenarioActivityAttachment: + title: ScenarioActivityAttachment + type: object + required: + - filename + - lastModifiedBy + - lastModifiedAt + properties: + id: + type: + - integer + - 'null' + format: int64 + filename: + type: string + lastModifiedBy: + type: string + lastModifiedAt: + type: string + format: date-time + ScenarioActivityComment: + title: ScenarioActivityComment + type: object + required: + - lastModifiedBy + - lastModifiedAt + properties: + comment: + type: + - string + - 'null' + lastModifiedBy: + type: string + lastModifiedAt: + type: string + format: date-time + ScenarioActivityMetadata: + title: ScenarioActivityMetadata + type: object + required: + - type + - displayableName + - icon + properties: + type: + type: string + displayableName: + type: string + icon: + type: string + supportedActions: type: array items: - $ref: '#/components/schemas/Comment' + type: string + ScenarioArchived: + title: ScenarioArchived + type: object + required: + - id + - user + - date + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + type: + type: string + ScenarioAttachments: + title: ScenarioAttachments + type: object + properties: attachments: type: array items: $ref: '#/components/schemas/Attachment' + ScenarioCanceled: + title: ScenarioCanceled + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string + ScenarioCreated: + title: ScenarioCreated + type: object + required: + - id + - user + - date + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + type: + type: string + ScenarioDeployed: + title: ScenarioDeployed + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string + ScenarioModified: + title: ScenarioModified + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string + ScenarioNameChanged: + title: ScenarioNameChanged + type: object + required: + - id + - user + - date + - oldName + - newName + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + oldName: + type: string + newName: + type: string + type: + type: string ScenarioParameters: title: ScenarioParameters type: object @@ -4453,6 +5984,57 @@ components: $ref: '#/components/schemas/ScenarioParameters' engineSetupErrors: $ref: '#/components/schemas/Map_EngineSetupName_List_String' + ScenarioPaused: + title: ScenarioPaused + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string + ScenarioUnarchived: + title: ScenarioUnarchived + type: object + required: + - id + - user + - date + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + type: + type: string ScenarioUsageData: title: ScenarioUsageData type: object From 05c7a58df3b345dd13490cd533c0f03f974a7c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 9 Sep 2024 15:04:10 +0200 Subject: [PATCH 15/43] qs --- .../api/ScenarioActivityApiHttpService.scala | 78 +++++++++---------- 1 file changed, 39 insertions(+), 39 deletions(-) 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 486ad53dd3b..062c88c1f41 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 @@ -93,6 +93,31 @@ class ScenarioActivityApiHttpService( } } + expose { + endpoints.addAttachmentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: AddAttachmentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Write) + _ <- saveAttachment(request, scenarioId) + } yield () + } + } + + expose { + endpoints.downloadAttachmentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: GetAttachmentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Read) + maybeAttachment <- EitherT.right(attachmentService.readAttachment(request.attachmentId, scenarioId)) + response = buildResponse(maybeAttachment) + } yield response + } + } + expose { endpoints.scenarioActivitiesEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) @@ -100,11 +125,23 @@ class ScenarioActivityApiHttpService( for { scenarioId <- getScenarioIdByName(scenarioName) _ <- isAuthorized(scenarioId, Permission.Read) - activities <- EitherT.liftF(Future.failed(new Exception("API not yet implemented"))) + activities <- notImplemented[List[ScenarioActivity]] } yield ScenarioActivities(activities) } } + expose { + endpoints.attachmentsEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => processName: ProcessName => + for { + scenarioId <- getScenarioIdByName(processName) + _ <- isAuthorized(scenarioId, Permission.Read) + attachments <- notImplemented[ScenarioAttachments] + } yield attachments + } + } + expose { endpoints.scenarioActivitiesMetadataEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) @@ -124,7 +161,7 @@ class ScenarioActivityApiHttpService( for { scenarioId <- getScenarioIdByName(request.scenarioName) _ <- isAuthorized(scenarioId, Permission.Write) - _ <- addNewComment(request, scenarioId) + _ <- notImplemented[Unit] } yield () } } @@ -153,43 +190,6 @@ class ScenarioActivityApiHttpService( } } - expose { - endpoints.attachmentsEndpoint - .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) - .serverLogicEitherT { implicit loggedUser => processName: ProcessName => - for { - scenarioId <- getScenarioIdByName(processName) - _ <- isAuthorized(scenarioId, Permission.Read) - attachments <- notImplemented[ScenarioAttachments] - } yield attachments - } - } - - expose { - endpoints.addAttachmentEndpoint - .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) - .serverLogicEitherT { implicit loggedUser => request: AddAttachmentRequest => - for { - scenarioId <- getScenarioIdByName(request.scenarioName) - _ <- isAuthorized(scenarioId, Permission.Write) - _ <- saveAttachment(request, scenarioId) - } yield () - } - } - - expose { - endpoints.downloadAttachmentEndpoint - .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) - .serverLogicEitherT { implicit loggedUser => request: GetAttachmentRequest => - for { - scenarioId <- getScenarioIdByName(request.scenarioName) - _ <- isAuthorized(scenarioId, Permission.Read) - maybeAttachment <- EitherT.right(attachmentService.readAttachment(request.attachmentId, scenarioId)) - response = buildResponse(maybeAttachment) - } yield response - } - } - private def notImplemented[T]: EitherT[Future, ScenarioActivityError, T] = EitherT.leftT[Future, T](ScenarioActivityError.NotImplemented: ScenarioActivityError) From e101258568df50eb5e1b18189913edd0a6cc1de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 9 Sep 2024 16:10:10 +0200 Subject: [PATCH 16/43] qs --- ...DesignerApiAvailableToExposeYamlSpec.scala | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala index 7c0bc88ff69..dc1ee5124c6 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala @@ -27,22 +27,23 @@ import scala.util.Try // Warning! OpenAPI can be generated differently depending on the scala version. class NuDesignerApiAvailableToExposeYamlSpec extends AnyFunSuite with Matchers { - test("Nu Designer OpenAPI document with all available to expose endpoints should have examples matching schemas") { - val generatedSpec = NuDesignerApiAvailableToExpose.generateOpenApiYaml - val examplesValidationResult = OpenAPIExamplesValidator.forTapir.validateExamples(generatedSpec) - val clue = examplesValidationResult - .map { case InvalidExample(_, _, operationId, isRequest, exampleId, errors) => - errors - .map(_.getMessage) - .distinct - .map(" " + _) - .mkString(s"$operationId > ${if (isRequest) "request" else "response"} > $exampleId\n", "\n", "") - } - .mkString("", "\n", "\n") - withClue(clue) { - examplesValidationResult.size shouldEqual 0 - } - } +// todo NU-1772: the JSON schema validation does not correctly handle the responses with discriminator +// test("Nu Designer OpenAPI document with all available to expose endpoints should have examples matching schemas") { +// val generatedSpec = NuDesignerApiAvailableToExpose.generateOpenApiYaml +// val examplesValidationResult = OpenAPIExamplesValidator.forTapir.validateExamples(generatedSpec) +// val clue = examplesValidationResult +// .map { case InvalidExample(_, _, operationId, isRequest, exampleId, errors) => +// errors +// .map(_.getMessage) +// .distinct +// .map(" " + _) +// .mkString(s"$operationId > ${if (isRequest) "request" else "response"} > $exampleId\n", "\n", "") +// } +// .mkString("", "\n", "\n") +// withClue(clue) { +// examplesValidationResult.size shouldEqual 0 +// } +// } test("Nu Designer OpenAPI document with all available to expose endpoints has to be up to date") { val currentNuDesignerOpenApiYamlContent = From 3a8aca4b38c46c9f4d3a6e1d98c5e26595243c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 10 Sep 2024 15:14:22 +0200 Subject: [PATCH 17/43] qs --- .../test/utils/OpenAPIExamplesValidator.scala | 9 +- ...DesignerApiAvailableToExposeYamlSpec.scala | 38 +-- docs-internal/api/nu-designer-openapi.yaml | 216 +++++++++--------- 3 files changed, 136 insertions(+), 127 deletions(-) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/OpenAPIExamplesValidator.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/OpenAPIExamplesValidator.scala index c5e2a0be450..bd77d50a291 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/OpenAPIExamplesValidator.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/OpenAPIExamplesValidator.scala @@ -13,7 +13,10 @@ class OpenAPIExamplesValidator private (schemaFactory: JsonSchemaFactory) { import OpenAPIExamplesValidator._ - def validateExamples(specYaml: String): List[InvalidExample] = { + def validateExamples( + specYaml: String, + excludeResponseValidationForOperationIds: List[String] = List.empty + ): List[InvalidExample] = { val specJson = YamlParser.parse(specYaml).toOption.get val componentsSchemas = specJson.hcursor @@ -26,7 +29,9 @@ class OpenAPIExamplesValidator private (schemaFactory: JsonSchemaFactory) { (_, operation) <- pathItem.asObject.map(_.toList).getOrElse(List.empty) operationId = operation.hcursor.downField("operationId").as[String].toTry.get invalidExample <- validateRequestExample(operation, operationId, componentsSchemas) ::: - validateResponsesExamples(operation, operationId, componentsSchemas) + (if (!excludeResponseValidationForOperationIds.contains(operationId)) + validateResponsesExamples(operation, operationId, componentsSchemas) + else List.empty) } yield invalidExample } diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala index dc1ee5124c6..04dcefdfdf7 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala @@ -27,23 +27,27 @@ import scala.util.Try // Warning! OpenAPI can be generated differently depending on the scala version. class NuDesignerApiAvailableToExposeYamlSpec extends AnyFunSuite with Matchers { -// todo NU-1772: the JSON schema validation does not correctly handle the responses with discriminator -// test("Nu Designer OpenAPI document with all available to expose endpoints should have examples matching schemas") { -// val generatedSpec = NuDesignerApiAvailableToExpose.generateOpenApiYaml -// val examplesValidationResult = OpenAPIExamplesValidator.forTapir.validateExamples(generatedSpec) -// val clue = examplesValidationResult -// .map { case InvalidExample(_, _, operationId, isRequest, exampleId, errors) => -// errors -// .map(_.getMessage) -// .distinct -// .map(" " + _) -// .mkString(s"$operationId > ${if (isRequest) "request" else "response"} > $exampleId\n", "\n", "") -// } -// .mkString("", "\n", "\n") -// withClue(clue) { -// examplesValidationResult.size shouldEqual 0 -// } -// } + test("Nu Designer OpenAPI document with all available to expose endpoints should have examples matching schemas") { + val generatedSpec = NuDesignerApiAvailableToExpose.generateOpenApiYaml + val examplesValidationResult = OpenAPIExamplesValidator.forTapir.validateExamples( + specYaml = generatedSpec, + excludeResponseValidationForOperationIds = List( + "getApiProcessesScenarionameActivityActivities" // todo NU-1772: responses contains discriminator, it is not properly handled by validator + ) + ) + val clue = examplesValidationResult + .map { case InvalidExample(_, _, operationId, isRequest, exampleId, errors) => + errors + .map(_.getMessage) + .distinct + .map(" " + _) + .mkString(s"$operationId > ${if (isRequest) "request" else "response"} > $exampleId\n", "\n", "") + } + .mkString("", "\n", "\n") + withClue(clue) { + examplesValidationResult.size shouldEqual 0 + } + } test("Nu Designer OpenAPI document with all available to expose endpoints has to be up to date") { val currentNuDesignerOpenApiYamlContent = diff --git a/docs-internal/api/nu-designer-openapi.yaml b/docs-internal/api/nu-designer-openapi.yaml index eeda77e9167..771bdf5f7db 100644 --- a/docs-internal/api/nu-designer-openapi.yaml +++ b/docs-internal/api/nu-designer-openapi.yaml @@ -2870,12 +2870,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/comment/{scenarioActivityId}: - put: + /api/processes/{scenarioName}/{versionId}/activity/comments: + post: tags: - Activities - summary: Edit process comment service - operationId: putApiProcessesScenarionameActivityCommentScenarioactivityid + summary: Add scenario comment service + operationId: postApiProcessesScenarionameVersionidActivityComments parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2889,12 +2889,12 @@ paths: required: true schema: type: string - - name: scenarioActivityId + - name: versionId in: path required: true schema: - type: string - format: uuid + type: integer + format: int64 requestBody: content: text/plain: @@ -2906,7 +2906,7 @@ paths: description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter scenarioActivityId, Invalid value for: body' + value for: path parameter versionId, Invalid value for: body' content: text/plain: schema: @@ -2943,16 +2943,6 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found - '500': - description: '' - content: - text/plain: - schema: - type: string - examples: - Example: - summary: 'Unable to edit comment with id: {commentId}' - value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2964,14 +2954,16 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation + deprecated: true security: - {} - httpAuth: [] + /api/processes/{scenarioName}/activity/comments/{commentId}: delete: tags: - Activities summary: Delete process comment service - operationId: deleteApiProcessesScenarionameActivityCommentScenarioactivityid + operationId: deleteApiProcessesScenarionameActivityCommentsCommentid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2985,18 +2977,18 @@ paths: required: true schema: type: string - - name: scenarioActivityId + - name: commentId in: path required: true schema: - type: string - format: uuid + type: integer + format: int64 responses: '200': description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter scenarioActivityId' + value for: path parameter commentId' content: text/plain: schema: @@ -3054,15 +3046,16 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation + deprecated: true security: - {} - httpAuth: [] - /api/processes/{scenarioName}/{versionId}/activity/comments: - post: + /api/processes/{scenarioName}/activity: + get: tags: - Activities - summary: Add scenario comment service - operationId: postApiProcessesScenarionameVersionidActivityComments + summary: Scenario activity service + operationId: getApiProcessesScenarionameActivity parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3076,24 +3069,31 @@ paths: required: true schema: type: string - - name: versionId - in: path - required: true - schema: - type: integer - format: int64 - requestBody: - content: - text/plain: - schema: - type: string - required: true responses: '200': description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessActivity' + examples: + Example: + summary: Display scenario activity + value: + comments: + - id: 1 + processVersionId: 1 + content: some comment + user: test + createDate: '2024-01-17T14:21:17Z' + attachments: + - id: 1 + processVersionId: 1 + fileName: some_file.txt + user: test + createDate: '2024-01-17T14:21:17Z' '400': - description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter versionId, Invalid value for: body' + description: 'Invalid value for: header Nu-Impersonate-User-Identity' content: text/plain: schema: @@ -3145,12 +3145,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/comments/{commentId}: - delete: + /api/processes/{scenarioName}/activity/attachments/{attachmentId}: + get: tags: - Activities - summary: Delete process comment service - operationId: deleteApiProcessesScenarionameActivityCommentsCommentid + summary: Download attachment service + operationId: getApiProcessesScenarionameActivityAttachmentsAttachmentid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3164,7 +3164,7 @@ paths: required: true schema: type: string - - name: commentId + - name: attachmentId in: path required: true schema: @@ -3173,9 +3173,25 @@ paths: responses: '200': description: '' + headers: + Content-Disposition: + required: false + schema: + type: + - string + - 'null' + Content-Type: + required: true + schema: + type: string + content: + application/octet-stream: + schema: + type: string + format: binary '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter commentId' + value for: path parameter attachmentId' content: text/plain: schema: @@ -3212,16 +3228,6 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found - '500': - description: '' - content: - text/plain: - schema: - type: string - examples: - Example: - summary: 'Unable to edit comment with id: {commentId}' - value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -3233,16 +3239,15 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation - deprecated: true security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity: - get: + /api/processes/{scenarioName}/activity/comment/{scenarioActivityId}: + put: tags: - Activities - summary: Scenario activity service - operationId: getApiProcessesScenarionameActivity + summary: Edit process comment service + operationId: putApiProcessesScenarionameActivityCommentScenarioactivityid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3256,31 +3261,24 @@ paths: required: true schema: type: string + - name: scenarioActivityId + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + text/plain: + schema: + type: string + required: true responses: '200': description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ProcessActivity' - examples: - Example: - summary: Display scenario activity - value: - comments: - - id: 1 - processVersionId: 1 - content: some comment - user: test - createDate: '2024-01-17T14:21:17Z' - attachments: - - id: 1 - processVersionId: 1 - fileName: some_file.txt - user: test - createDate: '2024-01-17T14:21:17Z' '400': - description: 'Invalid value for: header Nu-Impersonate-User-Identity' + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter scenarioActivityId, Invalid value for: body' content: text/plain: schema: @@ -3317,6 +3315,16 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -3328,16 +3336,14 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation - deprecated: true security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/attachments/{attachmentId}: - get: + delete: tags: - Activities - summary: Download attachment service - operationId: getApiProcessesScenarionameActivityAttachmentsAttachmentid + summary: Delete process comment service + operationId: deleteApiProcessesScenarionameActivityCommentScenarioactivityid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3351,34 +3357,18 @@ paths: required: true schema: type: string - - name: attachmentId + - name: scenarioActivityId in: path required: true schema: - type: integer - format: int64 + type: string + format: uuid responses: '200': description: '' - headers: - Content-Disposition: - required: false - schema: - type: - - string - - 'null' - Content-Type: - required: true - schema: - type: string - content: - application/octet-stream: - schema: - type: string - format: binary '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter attachmentId' + value for: path parameter scenarioActivityId' content: text/plain: schema: @@ -3415,6 +3405,16 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: From 874206ef27f6900ddda677972a0ba1bf90ec899e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 10 Sep 2024 15:32:19 +0200 Subject: [PATCH 18/43] qs --- ...DesignerApiAvailableToExposeYamlSpec.scala | 14 +- docs-internal/api/nu-designer-openapi.yaml | 216 +++++++++--------- 2 files changed, 118 insertions(+), 112 deletions(-) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala index 04dcefdfdf7..cd42fe7b2e3 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala @@ -32,7 +32,7 @@ class NuDesignerApiAvailableToExposeYamlSpec extends AnyFunSuite with Matchers { val examplesValidationResult = OpenAPIExamplesValidator.forTapir.validateExamples( specYaml = generatedSpec, excludeResponseValidationForOperationIds = List( - "getApiProcessesScenarionameActivityActivities" // todo NU-1772: responses contains discriminator, it is not properly handled by validator + "getApiProcessesScenarionameActivityActivities" // todo NU-1772: responses contain discriminator, it is not properly handled by validator ) ) val clue = examplesValidationResult @@ -50,9 +50,15 @@ class NuDesignerApiAvailableToExposeYamlSpec extends AnyFunSuite with Matchers { } test("Nu Designer OpenAPI document with all available to expose endpoints has to be up to date") { - val currentNuDesignerOpenApiYamlContent = - (Project.root / "docs-internal" / "api" / "nu-designer-openapi.yaml").contentAsString - NuDesignerApiAvailableToExpose.generateOpenApiYaml should be(currentNuDesignerOpenApiYamlContent) + // todo NU-1772: OpenAPI differs when generated on Scala 2.12 and Scala 2.13 (order of endpoints is different) + // test is for now ignored on Scala 2.12 + if (scala.util.Properties.versionNumberString.startsWith("2.13")) { + val currentNuDesignerOpenApiYamlContent = + (Project.root / "docs-internal" / "api" / "nu-designer-openapi.yaml").contentAsString + NuDesignerApiAvailableToExpose.generateOpenApiYaml should be(currentNuDesignerOpenApiYamlContent) + } else { + info("OpenAPI differs when generated on Scala 2.12 and Scala 2.13. Test is ignored on Scala 2.12") + } } test("API enum compatibility test") { diff --git a/docs-internal/api/nu-designer-openapi.yaml b/docs-internal/api/nu-designer-openapi.yaml index 771bdf5f7db..eeda77e9167 100644 --- a/docs-internal/api/nu-designer-openapi.yaml +++ b/docs-internal/api/nu-designer-openapi.yaml @@ -2870,12 +2870,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/{versionId}/activity/comments: - post: + /api/processes/{scenarioName}/activity/comment/{scenarioActivityId}: + put: tags: - Activities - summary: Add scenario comment service - operationId: postApiProcessesScenarionameVersionidActivityComments + summary: Edit process comment service + operationId: putApiProcessesScenarionameActivityCommentScenarioactivityid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2889,12 +2889,12 @@ paths: required: true schema: type: string - - name: versionId + - name: scenarioActivityId in: path required: true schema: - type: integer - format: int64 + type: string + format: uuid requestBody: content: text/plain: @@ -2906,7 +2906,7 @@ paths: description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter versionId, Invalid value for: body' + value for: path parameter scenarioActivityId, Invalid value for: body' content: text/plain: schema: @@ -2943,6 +2943,16 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2954,16 +2964,14 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation - deprecated: true security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/comments/{commentId}: delete: tags: - Activities summary: Delete process comment service - operationId: deleteApiProcessesScenarionameActivityCommentsCommentid + operationId: deleteApiProcessesScenarionameActivityCommentScenarioactivityid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2977,18 +2985,18 @@ paths: required: true schema: type: string - - name: commentId + - name: scenarioActivityId in: path required: true schema: - type: integer - format: int64 + type: string + format: uuid responses: '200': description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter commentId' + value for: path parameter scenarioActivityId' content: text/plain: schema: @@ -3046,16 +3054,15 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation - deprecated: true security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity: - get: + /api/processes/{scenarioName}/{versionId}/activity/comments: + post: tags: - Activities - summary: Scenario activity service - operationId: getApiProcessesScenarionameActivity + summary: Add scenario comment service + operationId: postApiProcessesScenarionameVersionidActivityComments parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3069,31 +3076,24 @@ paths: required: true schema: type: string + - name: versionId + in: path + required: true + schema: + type: integer + format: int64 + requestBody: + content: + text/plain: + schema: + type: string + required: true responses: '200': description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ProcessActivity' - examples: - Example: - summary: Display scenario activity - value: - comments: - - id: 1 - processVersionId: 1 - content: some comment - user: test - createDate: '2024-01-17T14:21:17Z' - attachments: - - id: 1 - processVersionId: 1 - fileName: some_file.txt - user: test - createDate: '2024-01-17T14:21:17Z' '400': - description: 'Invalid value for: header Nu-Impersonate-User-Identity' + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter versionId, Invalid value for: body' content: text/plain: schema: @@ -3145,12 +3145,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/attachments/{attachmentId}: - get: + /api/processes/{scenarioName}/activity/comments/{commentId}: + delete: tags: - Activities - summary: Download attachment service - operationId: getApiProcessesScenarionameActivityAttachmentsAttachmentid + summary: Delete process comment service + operationId: deleteApiProcessesScenarionameActivityCommentsCommentid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3164,7 +3164,7 @@ paths: required: true schema: type: string - - name: attachmentId + - name: commentId in: path required: true schema: @@ -3173,25 +3173,9 @@ paths: responses: '200': description: '' - headers: - Content-Disposition: - required: false - schema: - type: - - string - - 'null' - Content-Type: - required: true - schema: - type: string - content: - application/octet-stream: - schema: - type: string - format: binary '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter attachmentId' + value for: path parameter commentId' content: text/plain: schema: @@ -3228,6 +3212,16 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -3239,15 +3233,16 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation + deprecated: true security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/comment/{scenarioActivityId}: - put: + /api/processes/{scenarioName}/activity: + get: tags: - Activities - summary: Edit process comment service - operationId: putApiProcessesScenarionameActivityCommentScenarioactivityid + summary: Scenario activity service + operationId: getApiProcessesScenarionameActivity parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3261,24 +3256,31 @@ paths: required: true schema: type: string - - name: scenarioActivityId - in: path - required: true - schema: - type: string - format: uuid - requestBody: - content: - text/plain: - schema: - type: string - required: true responses: '200': description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessActivity' + examples: + Example: + summary: Display scenario activity + value: + comments: + - id: 1 + processVersionId: 1 + content: some comment + user: test + createDate: '2024-01-17T14:21:17Z' + attachments: + - id: 1 + processVersionId: 1 + fileName: some_file.txt + user: test + createDate: '2024-01-17T14:21:17Z' '400': - description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter scenarioActivityId, Invalid value for: body' + description: 'Invalid value for: header Nu-Impersonate-User-Identity' content: text/plain: schema: @@ -3315,16 +3317,6 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found - '500': - description: '' - content: - text/plain: - schema: - type: string - examples: - Example: - summary: 'Unable to edit comment with id: {commentId}' - value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -3336,14 +3328,16 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation + deprecated: true security: - {} - httpAuth: [] - delete: + /api/processes/{scenarioName}/activity/attachments/{attachmentId}: + get: tags: - Activities - summary: Delete process comment service - operationId: deleteApiProcessesScenarionameActivityCommentScenarioactivityid + summary: Download attachment service + operationId: getApiProcessesScenarionameActivityAttachmentsAttachmentid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3357,18 +3351,34 @@ paths: required: true schema: type: string - - name: scenarioActivityId + - name: attachmentId in: path required: true schema: - type: string - format: uuid + type: integer + format: int64 responses: '200': description: '' + headers: + Content-Disposition: + required: false + schema: + type: + - string + - 'null' + Content-Type: + required: true + schema: + type: string + content: + application/octet-stream: + schema: + type: string + format: binary '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter scenarioActivityId' + value for: path parameter attachmentId' content: text/plain: schema: @@ -3405,16 +3415,6 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found - '500': - description: '' - content: - text/plain: - schema: - type: string - examples: - Example: - summary: 'Unable to edit comment with id: {commentId}' - value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: From 5c5e25b7d8d8f567e7ac2e257479447dcb2253f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 10 Sep 2024 18:40:47 +0200 Subject: [PATCH 19/43] Data migration with test --- ...__CreateScenarioActivitiesDefinition.scala | 4 +- ...mmentsToScenarioActivitiesDefinition.scala | 27 +-- .../ScenarioActivityEntityFactory.scala | 4 +- .../process/newactivity/ActivityService.scala | 2 +- .../repository/ProcessActionRepository.scala | 2 +- .../repository/ProcessRepository.scala | 4 +- .../DbScenarioActivityRepository.scala | 6 +- ...tionsAndCommentsToScenarioActivities.scala | 180 +++++++++++++----- .../api/deployment/ProcessActivity.scala | 2 +- 9 files changed, 145 insertions(+), 86 deletions(-) diff --git a/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala index 5610004494e..0f7f5ae2e78 100644 --- a/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala +++ b/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala @@ -42,7 +42,7 @@ object V1_055__CreateScenarioActivitiesDefinition { def activityId: Rep[UUID] = column[UUID]("activity_id", NotNull) - def userId: Rep[String] = column[String]("user_id", NotNull) + def userId: Rep[Option[String]] = column[Option[String]]("user_id") def userName: Rep[String] = column[String]("user_name", NotNull) @@ -103,7 +103,7 @@ object V1_055__CreateScenarioActivitiesDefinition { activityType: String, scenarioId: Long, activityId: UUID, - userId: String, + userId: Option[String], userName: String, impersonatedByUserId: Option[String], impersonatedByUserName: Option[String], diff --git a/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala index 8a642c98236..8511a371567 100644 --- a/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala +++ b/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala @@ -16,7 +16,6 @@ import slick.lifted.{ProvenShape, TableQuery => LTableQuery} import slick.sql.SqlProfile.ColumnOption.NotNull import java.sql.Timestamp -import java.time.Instant import java.util.UUID import scala.concurrent.ExecutionContext.Implicits.global @@ -43,28 +42,6 @@ object V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends L def migrateActions: DBIOAction[(List[ScenarioActivityEntityData], Int), NoStream, Effect.All] = { logger.info("Executing migration V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition") for { - _ <- scenarioActivitiesDefinitions.scenarioActivitiesTable += ScenarioActivityEntityData( - id = -1L, - activityType = "QWERTY", - scenarioId = 1, - activityId = UUID.randomUUID(), - userId = "user", - userName = "user", - impersonatedByUserId = Some("user"), - impersonatedByUserName = Some("user"), - lastModifiedByUserName = Some("user"), - createdAt = Timestamp.from(Instant.now), - scenarioVersion = Some(1), - comment = None, - attachmentId = None, - finishedAt = Some(Timestamp.from(Instant.now)), - state = None, - errorMessage = None, - buildInfo = None, - additionalProperties = Map.empty[String, String].asJson.noSpaces - ) - _ <- - sqlu"""insert into public.processes (name, description, category, processing_type, is_fragment, is_archived, id, created_at, created_by, impersonated_by_identity, impersonated_by_username, latest_version_id, latest_finished_action_id, latest_finished_cancel_action_id, latest_finished_deploy_action_id) values ('2024_Q3_6917_NETFLIX', null, 'BatchPeriodic', 'streaming-batch-periodic', false, false, 141, '2024-09-02 11:01:24.564191', 'Łukasz Ciołecki', null, null, null, null, null, null)""" actionsWithComments <- processActionsDefinitions.table .joinLeft(commentsDefinitions.table) @@ -78,11 +55,11 @@ object V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends L activityType = activityTypeStr(processAction.actionName), scenarioId = processAction.processId, activityId = processAction.id, - userId = processAction.user, // todo + userId = None, userName = processAction.user, impersonatedByUserId = processAction.impersonatedByIdentity, impersonatedByUserName = processAction.impersonatedByUsername, - lastModifiedByUserName = processAction.impersonatedByUsername, + lastModifiedByUserName = Some(processAction.user), createdAt = processAction.createdAt, scenarioVersion = processAction.processVersionId, comment = maybeComment.map(_.content), 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 index 6a5973e77ee..94450388a54 100644 --- 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 @@ -33,7 +33,7 @@ trait ScenarioActivityEntityFactory extends BaseEntityFactory { def activityId: Rep[ScenarioActivityId] = column[ScenarioActivityId]("activity_id", NotNull) - def userId: Rep[String] = column[String]("user_id", NotNull) + def userId: Rep[Option[String]] = column[Option[String]]("user_id") def userName: Rep[String] = column[String]("user_name", NotNull) @@ -173,7 +173,7 @@ final case class ScenarioActivityEntityData( activityType: ScenarioActivityType, scenarioId: ProcessId, activityId: ScenarioActivityId, - userId: String, + userId: Option[String], userName: String, impersonatedByUserId: Option[String], impersonatedByUserName: Option[String], 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 933c992faec..edddde466f3 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 @@ -70,7 +70,7 @@ class ActivityService( scenarioId = ScenarioId(scenarioId.value), scenarioActivityId = ScenarioActivityId.random, user = ScenarioUser( - id = UserId(loggedUser.id), + id = Some(UserId(loggedUser.id)), name = UserName(loggedUser.username), impersonatedByUserId = loggedUser.impersonatingUserId.map(UserId.apply), impersonatedByUserName = loggedUser.impersonatingUserName.map(UserName.apply) 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 index 7cc355475f1..f0bbea7c1f5 100644 --- 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 @@ -245,7 +245,7 @@ class DbProcessActionRepository( activityType = activityType, scenarioId = processId, activityId = ScenarioActivityId(actionIdOpt.map(_.value).getOrElse(UUID.randomUUID())), - userId = user.id, + userId = Some(user.id), userName = user.username, impersonatedByUserId = user.impersonatingUserId, impersonatedByUserName = user.impersonatingUserName, 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 fec69aee696..4dc919a0a21 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 @@ -162,7 +162,7 @@ class DBProcessRepository( scenarioId = ScenarioId(scenarioId.value), scenarioActivityId = ScenarioActivityId.random, user = ScenarioUser( - id = UserId(loggedUser.id), + id = Some(UserId(loggedUser.id)), name = UserName(loggedUser.username), impersonatedByUserId = loggedUser.impersonatingUserId.map(UserId.apply), impersonatedByUserName = loggedUser.impersonatingUserName.map(UserName.apply) @@ -294,7 +294,7 @@ class DBProcessRepository( scenarioId = ScenarioId(process.id.value), scenarioActivityId = ScenarioActivityId.random, user = ScenarioUser( - id = UserId(loggedUser.id), + id = Some(UserId(loggedUser.id)), name = UserName(loggedUser.username), impersonatedByUserId = loggedUser.impersonatingUserId.map(UserId.apply), impersonatedByUserName = loggedUser.impersonatingUserName.map(UserName.apply) 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 index 0fda88917c2..daae9210181 100644 --- 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 @@ -298,7 +298,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( private def toUser(loggedUser: LoggedUser) = { ScenarioUser( - id = UserId(loggedUser.id), + id = Some(UserId(loggedUser.id)), name = UserName(loggedUser.username), impersonatedByUserId = loggedUser.impersonatingUserId.map(UserId.apply), impersonatedByUserName = loggedUser.impersonatingUserName.map(UserName.apply) @@ -431,7 +431,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( activityType = activityType, scenarioId = ProcessId(scenarioActivity.scenarioId.value), activityId = ScenarioActivityId.random, - userId = scenarioActivity.user.id.value, + userId = scenarioActivity.user.id.map(_.value), userName = scenarioActivity.user.name.value, impersonatedByUserId = scenarioActivity.user.impersonatedByUserId.map(_.value), impersonatedByUserName = scenarioActivity.user.impersonatedByUserName.map(_.value), @@ -583,7 +583,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( private def userFromEntity(entity: ScenarioActivityEntityData): ScenarioUser = { ScenarioUser( - id = UserId(entity.userId), + id = entity.userId.map(UserId), name = UserName(entity.userName), impersonatedByUserId = entity.impersonatedByUserId.map(UserId.apply), impersonatedByUserName = entity.impersonatedByUserName.map(UserName.apply), diff --git a/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala b/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala index e32638bb892..dece8bcf177 100644 --- a/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala +++ b/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala @@ -1,15 +1,28 @@ package db.migration +import db.migration.V1_055__CreateScenarioActivitiesDefinition.ScenarioActivityEntityData +import db.migration.V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition._ +import io.circe.syntax.EncoderOps import org.scalatest.freespec.AnyFreeSpecLike import org.scalatest.matchers.should.Matchers +import pl.touk.nussknacker.engine.api.deployment.ScenarioActionName +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.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 slick.jdbc.HsqldbProfile -import scala.concurrent.ExecutionContext.Implicits.global +import pl.touk.nussknacker.ui.db.NuTables +import pl.touk.nussknacker.ui.db.entity.{AdditionalProperties, ProcessEntityData, ProcessVersionEntityData} +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_056__MigrateActionsAndCommentsToScenarioActivities @@ -17,58 +30,127 @@ class V1_056__MigrateActionsAndCommentsToScenarioActivities with Matchers with NuItTest with WithSimplifiedDesignerConfig - with WithHsqlDbTesting { - -// val comments = new V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.CommentsDefinitions(HsqldbProfile) -// testDbRef.db.run( -// (comments.table ++ V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.CommentEntityData( -// id = ???, processId = ???, processVersionId = ???, content = ???, user = ???, impersonatedByIdentity = ???, impersonatedByUsername = ???, createDate = ??? -// ) -// ) - "When data is present in old actions table" - { + with WithHsqlDbTesting + with NuTables { + + override protected val profile: JdbcProfile = HsqldbProfile + + "When data is present in old actions and comments tables" - { "migrate data to scenario_activities table" in { import HsqldbProfile.api._ val runner = newDBIOActionRunner(testDbRef) -// session -// .prepareStatement( -// """ -// |INSERT INTO public.process_actions (process_version_id, "user", performed_at, build_info, action_name, process_id, comment_id, id, state, created_at, failure_message, impersonated_by_identity, impersonated_by_username) VALUES (12, 'Maciej Cichanowicz', '2024-06-27 15:07:33.015466', null, 'run now', 110, 572, '10954b4f-2d30-478b-afd7-ab27e3da32f4', 'FINISHED', '2024-06-27 15:07:26.554604', null, 'Admin App', 'Admin App'); -// |INSERT INTO public.process_actions (process_version_id, "user", performed_at, build_info, action_name, process_id, comment_id, id, state, created_at, failure_message, impersonated_by_identity, impersonated_by_username) VALUES (12, 'Maciej Cichanowicz', '2024-06-27 15:07:57.080057', null, 'CANCEL', 110, null, 'a7cb8478-055e-4fd5-bfa8-34ee2eb7bba8', 'FINISHED', '2024-06-27 15:07:56.895031', null, null, null); -// |INSERT INTO public.process_actions (process_version_id, "user", performed_at, build_info, action_name, process_id, comment_id, id, state, created_at, failure_message, impersonated_by_identity, impersonated_by_username) VALUES (12, 'Maciej Cichanowicz', '2024-06-27 15:07:59.845724', e'{ -// | "nussknacker-buildTime" : "2024-06-23T15:35:35.475741", -// | "nussknacker-gitCommit" : "7407c2b36d8af87293c73503a9447977673156a3", -// | "nussknacker-name" : "nussknacker-common-api", -// | "nussknacker-version" : "1.15.2-preview_1.15-esp-2024-06-23-18932-7407c2b36-SNAPSHOT", -// | "process-buildTime" : "2024-06-27T10:55:18.240067", -// | "process-gitCommit" : "47dea52bc0ff01da9c3e46e98c3323b3af2ea46d", -// | "process-name" : "integration", -// | "process-version" : "47dea52bc0ff01da9c3e46e98c3323b3af2ea46d-SNAPSHOT" -// |}', 'DEPLOY', 110, null, 'ddfcf387-00aa-48e8-a15b-6f265edafebe', 'FINISHED', '2024-06-27 15:07:59.225310', null, null, null); -// |INSERT INTO public.process_actions (process_version_id, "user", performed_at, build_info, action_name, process_id, comment_id, id, state, created_at, failure_message, impersonated_by_identity, impersonated_by_username) VALUES (13, 'Maciej Cichanowicz', '2024-06-27 15:09:32.114327', e'{ -// | "nussknacker-buildTime" : "2024-06-23T15:35:35.475741", -// | "nussknacker-gitCommit" : "7407c2b36d8af87293c73503a9447977673156a3", -// | "nussknacker-name" : "nussknacker-common-api", -// | "nussknacker-version" : "1.15.2-preview_1.15-esp-2024-06-23-18932-7407c2b36-SNAPSHOT", -// | "process-buildTime" : "2024-06-27T10:55:18.240067", -// | "process-gitCommit" : "47dea52bc0ff01da9c3e46e98c3323b3af2ea46d", -// | "process-name" : "integration", -// | "process-version" : "47dea52bc0ff01da9c3e46e98c3323b3af2ea46d-SNAPSHOT" -// |}', 'DEPLOY', 110, 574, 'b79e542d-13db-4086-ac98-e733f7fa3f9d', 'FINISHED', '2024-06-27 15:09:31.324140', null, 'Business Config', 'Business Config'); -// |INSERT INTO public.process_actions (process_version_id, "user", performed_at, build_info, action_name, process_id, comment_id, id, state, created_at, failure_message, impersonated_by_identity, impersonated_by_username) VALUES (11, 'Grzegorz Skrobisz', '2024-07-01 21:43:07.873472', null, 'run now', 104, null, '3529bcca-c3ca-4f77-8e8f-00e4e812fa7d', 'FINISHED', '2024-07-01 21:43:01.317651', null, null, null); -// |"""".stripMargin -// ) -// .execute() + + val migration = new Migration(HsqldbProfile) + val processActionsDefinitions = new ProcessActionsDefinitions(profile) + val commentsDefinitions = new CommentsDefinitions(profile) + + val now: Timestamp = Timestamp.from(Instant.now) + val user = "John Doe" + val versionId = VersionId(5L) + val actionId = UUID.randomUUID() + val commentId = 765L + + val processInsertQuery = processesTable returning processesTable.map(_.id) into ((item, id) => item.copy(id = id)) + + val processEntity = ProcessEntityData( + id = ProcessId(-1L), + name = ProcessName("2024_Q3_6917_NETFLIX"), + processCategory = "test-category", + description = None, + processingType = "BatchPeriodic", + isFragment = false, + isArchived = false, + createdAt = now, + createdBy = user, + impersonatedByIdentity = None, + impersonatedByUsername = None + ) + + def processVersionEntity(processEntity: ProcessEntityData) = ProcessVersionEntityData( + id = versionId, + 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), + ) + + def commentEntity(processEntity: ProcessEntityData) = CommentEntityData( + id = commentId, + processId = processEntity.id.value, + processVersionId = versionId.value, + content = "Very important change", + user = user, + impersonatedByIdentity = None, + impersonatedByUsername = None, + createDate = now, + ) + + def processActionEntity(processEntity: ProcessEntityData) = ProcessActionEntityData( + id = actionId, + processId = processEntity.id.value, + processVersionId = Some(versionId.value), + user = user, + impersonatedByIdentity = None, + impersonatedByUsername = None, + createdAt = now, + performedAt = None, + actionName = ScenarioActionName.Deploy.value, + state = "IN_PROGRESS", + failureMessage = None, + commentId = Some(commentId), + buildInfo = None + ) val dbOperations = for { - _ <- - sqlu"""INSERT INTO processes (name, description, category, processing_type, is_fragment, is_archived, id, created_at, created_by, impersonated_by_identity, impersonated_by_username, latest_version_id, latest_finished_action_id, latest_finished_cancel_action_id, latest_finished_deploy_action_id) VALUES ('2024_Q3_6917_NETFLIX', null, 'BatchPeriodic', 'streaming-batch-periodic', false, false, 141, '2024-09-02 11:01:24.564191', 'Some User', null, null, null, null, null, null);""" - _ <- - sqlu"""INSERT INTO process_comments (process_version_id, content, `user`, create_date, id, process_id, impersonated_by_identity, impersonated_by_username) VALUES (2, 'Deployment: komentarz przy deployu', 'admin', '2024-05-21 12:22:49.528439', 480, 104, null, null);""" - result <- new V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.Migration( - HsqldbProfile - ).migrateActions - } yield (result) - Await.ready(runner.run(dbOperations), Duration.Inf) + process <- processInsertQuery += processEntity + _ <- processVersionsTable += processVersionEntity(process) + _ <- commentsDefinitions.table += commentEntity(process) + _ <- processActionsDefinitions.table += processActionEntity(process) + result <- migration.migrateActions + } yield result._1 + + val activitiesCreatedDuringMigration = Await.result(runner.run(dbOperations), Duration.Inf) + + activitiesCreatedDuringMigration shouldBe + List( + ScenarioActivityEntityData( + id = -1, + activityType = "SCENARIO_DEPLOYED", + scenarioId = 1, + activityId = actionId, + userId = None, + userName = user, + impersonatedByUserId = None, + impersonatedByUserName = None, + lastModifiedByUserName = Some(user), + createdAt = now, + scenarioVersion = Some(versionId.value), + comment = Some("Very important change"), + attachmentId = None, + finishedAt = None, + state = Some("IN_PROGRESS"), + errorMessage = None, + buildInfo = None, + additionalProperties = AdditionalProperties.empty.properties.asJson.noSpaces, + ) + ) + } } diff --git a/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala index 5046b32eb85..2b3d501c0f2 100644 --- a/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala +++ b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala @@ -16,7 +16,7 @@ object ScenarioActivityId { } final case class ScenarioUser( - id: UserId, + id: Option[UserId], name: UserName, impersonatedByUserId: Option[UserId], impersonatedByUserName: Option[UserName], From ec5963e3a51174a62f5a488aa5c1a2c780d29327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 10 Sep 2024 18:55:14 +0200 Subject: [PATCH 20/43] qs --- .../V1_056__MigrateActionsAndCommentsToScenarioActivities.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala b/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala index dece8bcf177..f3aa0e4a3c9 100644 --- a/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala +++ b/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala @@ -38,6 +38,7 @@ class V1_056__MigrateActionsAndCommentsToScenarioActivities "When data is present in old actions and comments tables" - { "migrate data to scenario_activities table" in { import HsqldbProfile.api._ + val runner = newDBIOActionRunner(testDbRef) val migration = new Migration(HsqldbProfile) From 9113a0f41255977f4bcbd74309e501f4f1708f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 10 Sep 2024 20:20:52 +0200 Subject: [PATCH 21/43] fix after merge with API spec --- ...DesignerApiAvailableToExposeYamlSpec.scala | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala index 8a3e3c0ac93..cd42fe7b2e3 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala @@ -27,28 +27,27 @@ import scala.util.Try // Warning! OpenAPI can be generated differently depending on the scala version. class NuDesignerApiAvailableToExposeYamlSpec extends AnyFunSuite with Matchers { -// todo NU-1772: the JSON schema validation does not correctly handle the responses with discriminator -// test("Nu Designer OpenAPI document with all available to expose endpoints should have examples matching schemas") { -// val generatedSpec = NuDesignerApiAvailableToExpose.generateOpenApiYaml -// val examplesValidationResult = OpenAPIExamplesValidator.forTapir.validateExamples( + test("Nu Designer OpenAPI document with all available to expose endpoints should have examples matching schemas") { + val generatedSpec = NuDesignerApiAvailableToExpose.generateOpenApiYaml + val examplesValidationResult = OpenAPIExamplesValidator.forTapir.validateExamples( specYaml = generatedSpec, excludeResponseValidationForOperationIds = List( "getApiProcessesScenarionameActivityActivities" // todo NU-1772: responses contain discriminator, it is not properly handled by validator ) ) -// val clue = examplesValidationResult -// .map { case InvalidExample(_, _, operationId, isRequest, exampleId, errors) => -// errors -// .map(_.getMessage) -// .distinct -// .map(" " + _) -// .mkString(s"$operationId > ${if (isRequest) "request" else "response"} > $exampleId\n", "\n", "") -// } -// .mkString("", "\n", "\n") -// withClue(clue) { -// examplesValidationResult.size shouldEqual 0 -// } -// } + val clue = examplesValidationResult + .map { case InvalidExample(_, _, operationId, isRequest, exampleId, errors) => + errors + .map(_.getMessage) + .distinct + .map(" " + _) + .mkString(s"$operationId > ${if (isRequest) "request" else "response"} > $exampleId\n", "\n", "") + } + .mkString("", "\n", "\n") + withClue(clue) { + examplesValidationResult.size shouldEqual 0 + } + } test("Nu Designer OpenAPI document with all available to expose endpoints has to be up to date") { // todo NU-1772: OpenAPI differs when generated on Scala 2.12 and Scala 2.13 (order of endpoints is different) From 909c3e776f5fd8e28fb02e086766c179f2b38bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 10 Sep 2024 22:48:22 +0200 Subject: [PATCH 22/43] Fixed tests --- .../nussknacker/ui/db/migration/SlickMigration.scala | 1 - .../activities/DbScenarioActivityRepository.scala | 10 +--------- ...MigrateActionsAndCommentsToScenarioActivities.scala | 9 ++++----- .../pl/touk/nussknacker/test/base/db/DbTesting.scala | 3 +-- 4 files changed, 6 insertions(+), 17 deletions(-) diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/migration/SlickMigration.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/migration/SlickMigration.scala index 5b989102dc4..1f322a12d00 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/migration/SlickMigration.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/migration/SlickMigration.scala @@ -40,7 +40,6 @@ trait ProcessJsonMigration extends SlickMigration with NuTables with LazyLogging override protected def migrateActions : DBIOAction[Seq[Int], NoStream, Effect.Read with Effect.Read with Effect.Write] = { - logger.error("Migration") for { allVersionIds <- processVersionsTableWithUnit.map(pve => (pve.id, pve.processId)).result updated <- DBIOAction.sequence(allVersionIds.zipWithIndex.map { case ((id, processId), scenarioIndex) => 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 index daae9210181..6b57b20356d 100644 --- 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 @@ -278,17 +278,9 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( } def getActivityStats: DB[Map[String, Int]] = { - val activityTypesWithLegacyComments = Set( - ScenarioActivityType.ScenarioDeployed, - ScenarioActivityType.ScenarioPaused, - ScenarioActivityType.ScenarioCanceled, - ScenarioActivityType.ScenarioModified, - ScenarioActivityType.ScenarioNameChanged, - ScenarioActivityType.CommentAdded, - ) val findScenarioProcessActivityStats = for { attachmentsTotal <- attachmentsTable.length.result - commentsTotal <- scenarioActivityTable.filter(_.activityType inSet activityTypesWithLegacyComments).length.result + commentsTotal <- scenarioActivityTable.filter(_.comment.isDefined).length.result } yield Map( AttachmentsTotal -> attachmentsTotal, CommentsTotal -> commentsTotal, diff --git a/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala b/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala index f3aa0e4a3c9..5eb77120f3d 100644 --- a/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala +++ b/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala @@ -124,16 +124,15 @@ class V1_056__MigrateActionsAndCommentsToScenarioActivities _ <- commentsDefinitions.table += commentEntity(process) _ <- processActionsDefinitions.table += processActionEntity(process) result <- migration.migrateActions - } yield result._1 + } yield (process, result._1) - val activitiesCreatedDuringMigration = Await.result(runner.run(dbOperations), Duration.Inf) - - activitiesCreatedDuringMigration shouldBe + val (createdProcess, insertedActivities) = Await.result(runner.run(dbOperations), Duration.Inf) + insertedActivities shouldBe List( ScenarioActivityEntityData( id = -1, activityType = "SCENARIO_DEPLOYED", - scenarioId = 1, + scenarioId = createdProcess.id.value, activityId = actionId, userId = None, userName = user, 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 eb4e02e2755..d11685798cf 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 @@ -84,8 +84,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 "tags"""").execute() session.prepareStatement("""delete from "environments"""").execute() From ac3e8dbac96d3e7c819e0503ac1ad5c43618c6c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 9 Sep 2024 14:58:25 +0200 Subject: [PATCH 23/43] API specification --- .../api/ScenarioActivityApiHttpService.scala | 134 +- .../touk/nussknacker/ui/api/TapirCodecs.scala | 13 +- .../ScenarioActivityApiEndpoints.scala | 280 --- .../description/scenarioActivity/Dtos.scala | 528 +++++ .../scenarioActivity/Endpoints.scala | 192 ++ .../scenarioActivity/Examples.scala | 236 ++ .../scenarioActivity/InputOutput.scala | 30 + ...DesignerApiAvailableToExposeYamlSpec.scala | 36 +- docs-internal/api/nu-designer-openapi.yaml | 1930 +++++++++++++++-- 9 files changed, 2897 insertions(+), 482 deletions(-) delete mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ScenarioActivityApiEndpoints.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Dtos.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Endpoints.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Examples.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/InputOutput.scala 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 8a9e1112a0b..486ad53dd3b 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 @@ -5,13 +5,13 @@ import com.typesafe.scalalogging.LazyLogging 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.ScenarioActivityApiEndpoints -import pl.touk.nussknacker.ui.api.description.ScenarioActivityApiEndpoints.Dtos.ScenarioActivityError.{ +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError.{ NoComment, NoPermission, NoScenario } -import pl.touk.nussknacker.ui.api.description.ScenarioActivityApiEndpoints.Dtos._ +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.process.{ProcessService, ScenarioAttachmentService} import pl.touk.nussknacker.ui.security.api.{AuthManager, LoggedUser} @@ -29,29 +29,48 @@ class ScenarioActivityApiHttpService( scenarioService: ProcessService, scenarioAuthorizer: AuthorizeProcess, attachmentService: ScenarioAttachmentService, - streamEndpointProvider: TapirStreamEndpointProvider + streamEndpointProvider: TapirStreamEndpointProvider, )(implicit executionContext: ExecutionContext) extends BaseHttpService(authManager) with LazyLogging { - private val scenarioActivityApiEndpoints = new ScenarioActivityApiEndpoints( - authManager.authenticationEndpointInput() - ) + private val securityInput = authManager.authenticationEndpointInput() + + private val endpoints = new Endpoints(securityInput, streamEndpointProvider) expose { - scenarioActivityApiEndpoints.scenarioActivityEndpoint + endpoints.deprecatedScenarioActivityEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => scenarioName: ProcessName => for { - scenarioId <- getScenarioIdByName(scenarioName) - _ <- isAuthorized(scenarioId, Permission.Read) - scenarioActivity <- EitherT.right(scenarioActivityRepository.findActivity(scenarioId)) - } yield ScenarioActivity(scenarioActivity) + 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 + ) + } + ) } } expose { - scenarioActivityApiEndpoints.addCommentEndpoint + endpoints.deprecatedAddCommentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => request: AddCommentRequest => for { @@ -63,9 +82,9 @@ class ScenarioActivityApiHttpService( } expose { - scenarioActivityApiEndpoints.deleteCommentEndpoint + endpoints.deprecatedDeleteCommentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) - .serverLogicEitherT { implicit loggedUser => request: DeleteCommentRequest => + .serverLogicEitherT { implicit loggedUser => request: DeprecatedDeleteCommentRequest => for { scenarioId <- getScenarioIdByName(request.scenarioName) _ <- isAuthorized(scenarioId, Permission.Write) @@ -75,8 +94,79 @@ class ScenarioActivityApiHttpService( } expose { - scenarioActivityApiEndpoints - .addAttachmentEndpoint(streamEndpointProvider.streamBodyEndpointInput) + endpoints.scenarioActivitiesEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => scenarioName: ProcessName => + for { + scenarioId <- getScenarioIdByName(scenarioName) + _ <- isAuthorized(scenarioId, Permission.Read) + activities <- EitherT.liftF(Future.failed(new Exception("API not yet implemented"))) + } yield ScenarioActivities(activities) + } + } + + expose { + endpoints.scenarioActivitiesMetadataEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => scenarioName: ProcessName => + for { + scenarioId <- getScenarioIdByName(scenarioName) + _ <- isAuthorized(scenarioId, Permission.Read) + metadata = ScenarioActivitiesMetadata.default + } yield metadata + } + } + + expose { + endpoints.addCommentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: AddCommentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Write) + _ <- addNewComment(request, scenarioId) + } yield () + } + } + + expose { + endpoints.editCommentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: EditCommentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Write) + _ <- notImplemented[Unit] + } yield () + } + } + + expose { + endpoints.deleteCommentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: DeleteCommentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Write) + _ <- notImplemented[Unit] + } yield () + } + } + + expose { + endpoints.attachmentsEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => processName: ProcessName => + for { + scenarioId <- getScenarioIdByName(processName) + _ <- isAuthorized(scenarioId, Permission.Read) + attachments <- notImplemented[ScenarioAttachments] + } yield attachments + } + } + + expose { + endpoints.addAttachmentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => request: AddAttachmentRequest => for { @@ -88,8 +178,7 @@ class ScenarioActivityApiHttpService( } expose { - scenarioActivityApiEndpoints - .downloadAttachmentEndpoint(streamEndpointProvider.streamBodyEndpointOutput) + endpoints.downloadAttachmentEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) .serverLogicEitherT { implicit loggedUser => request: GetAttachmentRequest => for { @@ -101,6 +190,9 @@ class ScenarioActivityApiHttpService( } } + 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), @@ -127,10 +219,10 @@ class ScenarioActivityApiHttpService( scenarioActivityRepository.addComment(scenarioId, request.versionId, UserComment(request.commentContent)) ) - private def deleteComment(request: DeleteCommentRequest): EitherT[Future, ScenarioActivityError, Unit] = + private def deleteComment(request: DeprecatedDeleteCommentRequest): EitherT[Future, ScenarioActivityError, Unit] = EitherT( scenarioActivityRepository.deleteComment(request.commentId) - ).leftMap(_ => NoComment(request.commentId)) + ).leftMap(_ => NoComment(request.commentId.toString)) private def saveAttachment(request: AddAttachmentRequest, scenarioId: ProcessId)( implicit loggedUser: LoggedUser diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TapirCodecs.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TapirCodecs.scala index 5cf51602f58..52fbab1403d 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TapirCodecs.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/TapirCodecs.scala @@ -9,7 +9,7 @@ import pl.touk.nussknacker.engine.deployment.EngineSetupName import pl.touk.nussknacker.ui.server.HeadersSupport.{ContentDisposition, FileName} import sttp.tapir.Codec.PlainCodec import sttp.tapir.CodecFormat.TextPlain -import sttp.tapir.{Codec, CodecFormat, DecodeResult, Schema} +import sttp.tapir.{Codec, CodecFormat, DecodeResult, Schema, Validator} import java.net.URL @@ -133,4 +133,15 @@ object TapirCodecs { implicit val classSchema: Schema[Class[_]] = Schema.string[Class[_]] } + def enumSchema[T]( + items: List[T], + encoder: T => String, + ): Schema[T] = + Schema.string.validate( + Validator.enumeration( + items, + (i: T) => Some(encoder(i)), + ), + ) + } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ScenarioActivityApiEndpoints.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ScenarioActivityApiEndpoints.scala deleted file mode 100644 index db36ba1b867..00000000000 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/ScenarioActivityApiEndpoints.scala +++ /dev/null @@ -1,280 +0,0 @@ -package pl.touk.nussknacker.ui.api.description - -import derevo.circe.{decoder, encoder} -import derevo.derive -import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} -import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions -import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions.SecuredEndpoint -import pl.touk.nussknacker.security.AuthCredentials -import pl.touk.nussknacker.ui.api.BaseHttpService.CustomAuthorizationError -import pl.touk.nussknacker.ui.api.TapirCodecs -import pl.touk.nussknacker.ui.process.repository.DbProcessActivityRepository.{ - Attachment => DbAttachment, - Comment => DbComment, - ProcessActivity => DbProcessActivity -} -import pl.touk.nussknacker.ui.server.HeadersSupport.FileName -import sttp.model.StatusCode.{InternalServerError, NotFound, Ok} -import sttp.model.{HeaderNames, MediaType} -import sttp.tapir.EndpointIO.Example -import sttp.tapir._ -import sttp.tapir.derevo.schema -import sttp.tapir.json.circe.jsonBody - -import java.io.InputStream -import java.time.Instant - -class ScenarioActivityApiEndpoints(auth: EndpointInput[AuthCredentials]) extends BaseEndpointDefinitions { - - import ScenarioActivityApiEndpoints.Dtos.ScenarioActivityError._ - import ScenarioActivityApiEndpoints.Dtos._ - import TapirCodecs.ContentDispositionCodec._ - import TapirCodecs.HeaderCodec._ - import TapirCodecs.ScenarioNameCodec._ - import TapirCodecs.VersionIdCodec._ - - lazy val scenarioActivityEndpoint: SecuredEndpoint[ProcessName, ScenarioActivityError, ScenarioActivity, Any] = - baseNuApiEndpoint - .summary("Scenario activity service") - .tag("Scenario") - .get - .in("processes" / path[ProcessName]("scenarioName") / "activity") - .out( - statusCode(Ok).and( - jsonBody[ScenarioActivity].example( - Example.of( - summary = Some("Display scenario activity"), - value = ScenarioActivity( - comments = List( - Comment( - id = 1L, - processVersionId = 1L, - content = "some comment", - user = "test", - createDate = Instant.parse("2024-01-17T14:21:17Z") - ) - ), - attachments = List( - Attachment( - id = 1L, - processVersionId = 1L, - fileName = "some_file.txt", - user = "test", - createDate = Instant.parse("2024-01-17T14:21:17Z") - ) - ) - ) - ) - ) - ) - ) - .errorOut(scenarioNotFoundErrorOutput) - .withSecurity(auth) - - lazy val addCommentEndpoint: SecuredEndpoint[AddCommentRequest, ScenarioActivityError, Unit, Any] = - baseNuApiEndpoint - .summary("Add scenario comment service") - .tag("Scenario") - .post - .in( - ("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" - / "comments" / stringBody).mapTo[AddCommentRequest] - ) - .out(statusCode(Ok)) - .errorOut(scenarioNotFoundErrorOutput) - .withSecurity(auth) - - lazy val deleteCommentEndpoint: SecuredEndpoint[DeleteCommentRequest, ScenarioActivityError, Unit, Any] = - baseNuApiEndpoint - .summary("Delete process comment service") - .tag("Scenario") - .delete - .in( - ("processes" / path[ProcessName]("scenarioName") / "activity" / "comments" - / path[Long]("commentId")).mapTo[DeleteCommentRequest] - ) - .out(statusCode(Ok)) - .errorOut( - oneOf[ScenarioActivityError]( - oneOfVariantFromMatchType( - NotFound, - plainBody[NoScenario] - .example( - Example.of( - summary = Some("No scenario {scenarioName} found"), - value = NoScenario(ProcessName("'example scenario'")) - ) - ) - ), - oneOfVariantFromMatchType( - InternalServerError, - plainBody[NoComment] - .example( - Example.of( - summary = Some("Unable to delete comment with id: {commentId}"), - value = NoComment(1L) - ) - ) - ) - ) - ) - .withSecurity(auth) - - def addAttachmentEndpoint( - implicit streamBodyEndpoint: EndpointInput[InputStream] - ): SecuredEndpoint[AddAttachmentRequest, ScenarioActivityError, Unit, Any] = { - baseNuApiEndpoint - .summary("Add scenario attachment service") - .tag("Scenario") - .post - .in( - ( - "processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" - / "attachments" / streamBodyEndpoint / header[FileName](HeaderNames.ContentDisposition) - ).mapTo[AddAttachmentRequest] - ) - .out(statusCode(Ok)) - .errorOut(scenarioNotFoundErrorOutput) - .withSecurity(auth) - } - - def downloadAttachmentEndpoint( - implicit streamBodyEndpoint: EndpointOutput[InputStream] - ): SecuredEndpoint[GetAttachmentRequest, ScenarioActivityError, GetAttachmentResponse, Any] = { - baseNuApiEndpoint - .summary("Download attachment service") - .tag("Scenario") - .get - .in( - ("processes" / path[ProcessName]("processName") / "activity" / "attachments" - / path[Long]("attachmentId")).mapTo[GetAttachmentRequest] - ) - .out( - statusCode(Ok) - .and(streamBodyEndpoint) - .and(header(HeaderNames.ContentDisposition)(optionalHeaderCodec)) - .and(header(HeaderNames.ContentType)(requiredHeaderCodec)) - .mapTo[GetAttachmentResponse] - ) - .errorOut(scenarioNotFoundErrorOutput) - .withSecurity(auth) - } - - private lazy val scenarioNotFoundErrorOutput: EndpointOutput.OneOf[ScenarioActivityError, ScenarioActivityError] = - oneOf[ScenarioActivityError]( - oneOfVariantFromMatchType( - NotFound, - plainBody[NoScenario] - .example( - Example.of( - summary = Some("No scenario {scenarioName} found"), - value = NoScenario(ProcessName("'example scenario'")) - ) - ) - ) - ) - -} - -object ScenarioActivityApiEndpoints { - - object Dtos { - @derive(encoder, decoder, schema) - final case class ScenarioActivity private (comments: List[Comment], attachments: List[Attachment]) - - object ScenarioActivity { - - def apply(activity: DbProcessActivity): ScenarioActivity = - new ScenarioActivity( - comments = activity.comments.map(Comment.apply), - attachments = activity.attachments.map(Attachment.apply) - ) - - } - - @derive(encoder, decoder, schema) - final case class Comment private ( - id: Long, - processVersionId: Long, - content: String, - user: String, - createDate: Instant - ) - - object Comment { - - def apply(comment: DbComment): Comment = - new Comment( - id = comment.id, - processVersionId = comment.processVersionId.value, - content = comment.content, - user = comment.user, - createDate = comment.createDate - ) - - } - - @derive(encoder, decoder, schema) - final case class Attachment private ( - id: Long, - processVersionId: Long, - fileName: String, - user: String, - createDate: Instant - ) - - object Attachment { - - def apply(attachment: DbAttachment): Attachment = - new Attachment( - id = attachment.id, - processVersionId = attachment.processVersionId.value, - fileName = attachment.fileName, - user = attachment.user, - createDate = attachment.createDate - ) - - } - - final case class AddCommentRequest(scenarioName: ProcessName, versionId: VersionId, commentContent: String) - - final case class DeleteCommentRequest(scenarioName: ProcessName, commentId: Long) - - final case class AddAttachmentRequest( - scenarioName: ProcessName, - versionId: VersionId, - body: InputStream, - fileName: FileName - ) - - final case class GetAttachmentRequest(scenarioName: ProcessName, attachmentId: Long) - - final case class GetAttachmentResponse(inputStream: InputStream, fileName: Option[String], contentType: String) - - object GetAttachmentResponse { - val emptyResponse: GetAttachmentResponse = - GetAttachmentResponse(InputStream.nullInputStream(), None, MediaType.TextPlainUtf8.toString()) - } - - sealed trait ScenarioActivityError - - object ScenarioActivityError { - final case class NoScenario(scenarioName: ProcessName) extends ScenarioActivityError - final case object NoPermission extends ScenarioActivityError with CustomAuthorizationError - 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" - ) - - implicit val noCommentCodec: Codec[String, NoComment, CodecFormat.TextPlain] = - BaseEndpointDefinitions.toTextPlainCodecSerializationOnly[NoComment](e => - s"Unable to delete comment with id: ${e.commentId}" - ) - - } - - } - -} 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 new file mode 100644 index 00000000000..bbae3da1bb6 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Dtos.scala @@ -0,0 +1,528 @@ +package pl.touk.nussknacker.ui.api.description.scenarioActivity + +import derevo.circe.{decoder, encoder} +import derevo.derive +import enumeratum.EnumEntry.UpperSnakecase +import enumeratum.{Enum, EnumEntry} +import io.circe +import io.circe.generic.extras +import io.circe.generic.extras.semiauto.deriveConfiguredCodec +import io.circe.{Decoder, Encoder} +import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} +import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions +import pl.touk.nussknacker.ui.api.BaseHttpService.CustomAuthorizationError +import pl.touk.nussknacker.ui.api.TapirCodecs.enumSchema +import pl.touk.nussknacker.ui.server.HeadersSupport.FileName +import sttp.model.MediaType +import sttp.tapir._ +import sttp.tapir.derevo.schema +import sttp.tapir.generic.Configuration + +import java.io.InputStream +import java.time.Instant +import java.util.UUID +import scala.collection.immutable + +object Dtos { + + @derive(encoder, decoder, schema) + final case class ScenarioActivitiesMetadata( + activities: List[ScenarioActivityMetadata], + actions: List[ScenarioActivityActionMetadata], + ) + + object ScenarioActivitiesMetadata { + + val default: ScenarioActivitiesMetadata = ScenarioActivitiesMetadata( + activities = ScenarioActivityType.values.map(ScenarioActivityMetadata.from).toList, + actions = List( + ScenarioActivityActionMetadata( + id = "compare", + displayableName = "Compare", + icon = "/assets/states/error.svg" + ), + ScenarioActivityActionMetadata( + id = "delete_comment", + displayableName = "Delete", + icon = "/assets/states/error.svg" + ), + ScenarioActivityActionMetadata( + id = "edit_comment", + displayableName = "Edit", + icon = "/assets/states/error.svg" + ), + ScenarioActivityActionMetadata( + id = "download_attachment", + displayableName = "Download", + icon = "/assets/states/error.svg" + ), + ScenarioActivityActionMetadata( + id = "delete_attachment", + displayableName = "Delete", + icon = "/assets/states/error.svg" + ), + ) + ) + + } + + @derive(encoder, decoder, schema) + final case class ScenarioActivityActionMetadata( + id: String, + displayableName: String, + icon: String, + ) + + @derive(encoder, decoder, schema) + final case class ScenarioActivityMetadata( + `type`: String, + displayableName: String, + icon: String, + supportedActions: List[String], + ) + + object ScenarioActivityMetadata { + + def from(scenarioActivityType: ScenarioActivityType): ScenarioActivityMetadata = + ScenarioActivityMetadata( + `type` = scenarioActivityType.entryName, + displayableName = scenarioActivityType.displayableName, + icon = scenarioActivityType.icon, + supportedActions = scenarioActivityType.supportedActions, + ) + + } + + sealed trait ScenarioActivityType extends EnumEntry with UpperSnakecase { + def displayableName: String + def icon: String + def supportedActions: List[String] + } + + object ScenarioActivityType extends Enum[ScenarioActivityType] { + + case object ScenarioCreated extends ScenarioActivityType { + override def displayableName: String = "Scenario created" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioArchived extends ScenarioActivityType { + override def displayableName: String = "Scenario archived" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioUnarchived extends ScenarioActivityType { + override def displayableName: String = "Scenario unarchived" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioDeployed extends ScenarioActivityType { + override def displayableName: String = "Deployment" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioPaused extends ScenarioActivityType { + override def displayableName: String = "Pause" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioCanceled extends ScenarioActivityType { + override def displayableName: String = "Cancel" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ScenarioModified extends ScenarioActivityType { + override def displayableName: String = "New version saved" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List("compare") + } + + case object ScenarioNameChanged extends ScenarioActivityType { + override def displayableName: String = "Scenario name changed" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object CommentAdded extends ScenarioActivityType { + override def displayableName: String = "Comment" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List("delete_comment", "edit_comment") + } + + case object AttachmentAdded extends ScenarioActivityType { + override def displayableName: String = "Attachment" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object ChangedProcessingMode extends ScenarioActivityType { + override def displayableName: String = "Processing mode change" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object IncomingMigration extends ScenarioActivityType { + override def displayableName: String = "Incoming migration" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List("compare") + } + + case object OutgoingMigration extends ScenarioActivityType { + override def displayableName: String = "Outgoing migration" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object PerformedSingleExecution extends ScenarioActivityType { + override def displayableName: String = "Processing data" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object PerformedScheduledExecution extends ScenarioActivityType { + override def displayableName: String = "Processing data" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + case object AutomaticUpdate extends ScenarioActivityType { + override def displayableName: String = "Automatic update" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List("compare") + } + + case object CustomAction extends ScenarioActivityType { + override def displayableName: String = "Custom action" + override def icon: String = "/assets/states/error.svg" + override def supportedActions: List[String] = List.empty + } + + override def values: immutable.IndexedSeq[ScenarioActivityType] = findValues + + implicit def scenarioActivityTypeSchema: Schema[ScenarioActivityType] = + enumSchema[ScenarioActivityType]( + ScenarioActivityType.values.toList, + _.entryName, + ) + + implicit def scenarioActivityTypeCodec: circe.Codec[ScenarioActivityType] = circe.Codec.from( + Decoder.decodeString.emap(str => + ScenarioActivityType.withNameEither(str).left.map(_ => s"Invalid scenario action type [$str]") + ), + Encoder.encodeString.contramap(_.entryName), + ) + + implicit def scenarioActivityTypeTextCodec: Codec[String, ScenarioActivityType, CodecFormat.TextPlain] = + Codec.string.map( + Mapping.fromDecode[String, ScenarioActivityType] { + ScenarioActivityType.withNameOption(_) match { + case Some(value) => DecodeResult.Value(value) + case None => DecodeResult.InvalidValue(Nil) + } + }(_.entryName) + ) + + } + + @derive(encoder, decoder, schema) + final case class ScenarioActivityComment(comment: Option[String], lastModifiedBy: String, lastModifiedAt: Instant) + + @derive(encoder, decoder, schema) + final case class ScenarioActivityAttachment( + id: Option[Long], + filename: String, + lastModifiedBy: String, + lastModifiedAt: Instant + ) + + @derive(encoder, decoder, schema) + final case class ScenarioActivities(activities: List[ScenarioActivity]) + + sealed trait ScenarioActivity { + def id: UUID + def user: String + def date: Instant + def scenarioVersion: Option[Long] + } + + object ScenarioActivity { + + implicit def scenarioActivityCodec: circe.Codec[ScenarioActivity] = { + implicit val configuration: extras.Configuration = + extras.Configuration.default.withDiscriminator("type").withScreamingSnakeCaseConstructorNames + deriveConfiguredCodec + } + + implicit def scenarioActivitySchema: Schema[ScenarioActivity] = { + implicit val configuration: Configuration = + Configuration.default.withDiscriminator("type").withScreamingSnakeCaseDiscriminatorValues + Schema.derived[ScenarioActivity] + } + + final case class ScenarioCreated( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + ) extends ScenarioActivity + + final case class ScenarioArchived( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + ) extends ScenarioActivity + + final case class ScenarioUnarchived( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + ) extends ScenarioActivity + + // Scenario deployments + + final case class ScenarioDeployed( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + final case class ScenarioPaused( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + final case class ScenarioCanceled( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + // Scenario modifications + + final case class ScenarioModified( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + final case class ScenarioNameChanged( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + oldName: String, + newName: String, + ) extends ScenarioActivity + + final case class CommentAdded( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + ) extends ScenarioActivity + + final case class AttachmentAdded( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + attachment: ScenarioActivityAttachment, + ) extends ScenarioActivity + + final case class ChangedProcessingMode( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + from: String, + to: String, + ) extends ScenarioActivity + + // Migration between environments + + final case class IncomingMigration( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + sourceEnvironment: String, + sourceScenarioVersion: String, + ) extends ScenarioActivity + + final case class OutgoingMigration( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + comment: ScenarioActivityComment, + destinationEnvironment: String, + ) extends ScenarioActivity + + // Batch + + final case class PerformedSingleExecution( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + dateFinished: Instant, + errorMessage: Option[String], + ) extends ScenarioActivity + + final case class PerformedScheduledExecution( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + dateFinished: Instant, + errorMessage: Option[String], + ) extends ScenarioActivity + + // Other/technical + + final case class AutomaticUpdate( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + dateFinished: Instant, + changes: String, + errorMessage: Option[String], + ) extends ScenarioActivity + + final case class CustomAction( + id: UUID, + user: String, + date: Instant, + scenarioVersion: Option[Long], + actionName: String, + ) extends ScenarioActivity + + } + + @derive(encoder, decoder, schema) + final case class ScenarioAttachments(attachments: List[Attachment]) + + @derive(encoder, decoder, schema) + final case class Comment private ( + id: Long, + scenarioVersion: Long, + content: String, + user: String, + createDate: Instant + ) + + @derive(encoder, decoder, schema) + final case class Attachment private ( + id: Long, + scenarioVersion: Long, + fileName: String, + user: String, + createDate: Instant + ) + + final case class AddCommentRequest(scenarioName: ProcessName, versionId: VersionId, commentContent: String) + + final case class DeprecatedEditCommentRequest( + scenarioName: ProcessName, + commentId: Long, + commentContent: String + ) + + final case class EditCommentRequest( + scenarioName: ProcessName, + scenarioActivityId: UUID, + commentContent: String + ) + + final case class DeleteCommentRequest( + scenarioName: ProcessName, + scenarioActivityId: UUID + ) + + final case class DeprecatedDeleteCommentRequest( + scenarioName: ProcessName, + commentId: Long, + ) + + final case class AddAttachmentRequest( + scenarioName: ProcessName, + versionId: VersionId, + body: InputStream, + fileName: FileName + ) + + final case class GetAttachmentRequest(scenarioName: ProcessName, attachmentId: Long) + + final case class GetAttachmentResponse(inputStream: InputStream, fileName: Option[String], contentType: String) + + object GetAttachmentResponse { + val emptyResponse: GetAttachmentResponse = + GetAttachmentResponse(InputStream.nullInputStream(), None, MediaType.TextPlainUtf8.toString()) + } + + sealed trait ScenarioActivityError + + 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 + + implicit val noScenarioCodec: Codec[String, NoScenario, CodecFormat.TextPlain] = + BaseEndpointDefinitions.toTextPlainCodecSerializationOnly[NoScenario](e => s"No scenario ${e.scenarioName} found") + + implicit val noCommentCodec: Codec[String, NoComment, CodecFormat.TextPlain] = + BaseEndpointDefinitions.toTextPlainCodecSerializationOnly[NoComment](e => + s"Unable to delete comment with id: ${e.commentId}" + ) + + } + + object Legacy { + + @derive(encoder, decoder, schema) + final case class ProcessActivity private (comments: List[Comment], attachments: List[Attachment]) + + @derive(encoder, decoder, schema) + final case class Comment( + id: Long, + processVersionId: Long, + content: String, + user: String, + createDate: Instant + ) + + @derive(encoder, decoder, schema) + final case class Attachment( + id: Long, + processVersionId: Long, + fileName: String, + user: String, + createDate: Instant + ) + + } + +} 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 new file mode 100644 index 00000000000..a706ebd4dc4 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Endpoints.scala @@ -0,0 +1,192 @@ +package pl.touk.nussknacker.ui.api.description.scenarioActivity + +import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} +import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions +import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions.SecuredEndpoint +import pl.touk.nussknacker.security.AuthCredentials +import pl.touk.nussknacker.ui.api.TapirCodecs +import pl.touk.nussknacker.ui.server.HeadersSupport.FileName +import pl.touk.nussknacker.ui.server.TapirStreamEndpointProvider +import sttp.model.HeaderNames +import sttp.model.StatusCode.{InternalServerError, NotFound, Ok} +import sttp.tapir._ +import sttp.tapir.json.circe.jsonBody + +import java.util.UUID + +class Endpoints(auth: EndpointInput[AuthCredentials], streamProvider: TapirStreamEndpointProvider) + extends BaseEndpointDefinitions { + + import TapirCodecs.ContentDispositionCodec._ + import TapirCodecs.HeaderCodec._ + import TapirCodecs.ScenarioNameCodec._ + import TapirCodecs.VersionIdCodec._ + import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError._ + import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos._ + import pl.touk.nussknacker.ui.api.description.scenarioActivity.InputOutput._ + + lazy val deprecatedScenarioActivityEndpoint + : SecuredEndpoint[ProcessName, ScenarioActivityError, Legacy.ProcessActivity, Any] = + baseNuApiEndpoint + .summary("Scenario activity service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity") + .out(statusCode(Ok).and(jsonBody[Legacy.ProcessActivity].example(Examples.deprecatedScenarioActivity))) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + .deprecated() + + lazy val deprecatedAddCommentEndpoint: SecuredEndpoint[AddCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Add scenario comment service") + .tag("Activities") + .post + .in("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" / "comments") + .in(stringBody) + .mapInTo[AddCommentRequest] + .out(statusCode(Ok)) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + .deprecated() + + lazy val deprecatedDeleteCommentEndpoint + : SecuredEndpoint[DeprecatedDeleteCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Delete process comment service") + .tag("Activities") + .delete + .in( + "processes" / path[ProcessName]("scenarioName") / "activity" / "comments" / path[Long]("commentId") + ) + .mapInTo[DeprecatedDeleteCommentRequest] + .out(statusCode(Ok)) + .errorOut( + oneOf[ScenarioActivityError]( + oneOfVariantFromMatchType(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), + oneOfVariantFromMatchType(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)) + ) + ) + .withSecurity(auth) + .deprecated() + + lazy val scenarioActivitiesEndpoint: SecuredEndpoint[ + ProcessName, + ScenarioActivityError, + ScenarioActivities, + Any + ] = + baseNuApiEndpoint + .summary("Scenario activities service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity" / "activities") + .out(statusCode(Ok).and(jsonBody[ScenarioActivities].example(Examples.scenarioActivities))) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + + lazy val scenarioActivitiesMetadataEndpoint + : SecuredEndpoint[ProcessName, ScenarioActivityError, ScenarioActivitiesMetadata, Any] = + baseNuApiEndpoint + .summary("Scenario activities metadata service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity" / "activities" / "metadata") + .out(statusCode(Ok).and(jsonBody[ScenarioActivitiesMetadata].example(ScenarioActivitiesMetadata.default))) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + + lazy val addCommentEndpoint: SecuredEndpoint[AddCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Add scenario comment service") + .tag("Activities") + .post + .in("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" / "comment") + .in(stringBody) + .mapInTo[AddCommentRequest] + .out(statusCode(Ok)) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + + lazy val editCommentEndpoint: SecuredEndpoint[EditCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Edit process comment service") + .tag("Activities") + .put + .in( + "processes" / path[ProcessName]("scenarioName") / "activity" / "comment" / path[UUID]("scenarioActivityId") + ) + .in(stringBody) + .mapInTo[EditCommentRequest] + .out(statusCode(Ok)) + .errorOut( + oneOf[ScenarioActivityError]( + oneOfVariantFromMatchType(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), + oneOfVariantFromMatchType(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)) + ) + ) + .withSecurity(auth) + + lazy val deleteCommentEndpoint: SecuredEndpoint[DeleteCommentRequest, ScenarioActivityError, Unit, Any] = + baseNuApiEndpoint + .summary("Delete process comment service") + .tag("Activities") + .delete + .in( + "processes" / path[ProcessName]("scenarioName") / "activity" / "comment" / path[UUID]("scenarioActivityId") + ) + .mapInTo[DeleteCommentRequest] + .out(statusCode(Ok)) + .errorOut( + oneOf[ScenarioActivityError]( + oneOfVariantFromMatchType(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), + oneOfVariantFromMatchType(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)) + ) + ) + .withSecurity(auth) + + val attachmentsEndpoint: SecuredEndpoint[ProcessName, ScenarioActivityError, ScenarioAttachments, Any] = { + baseNuApiEndpoint + .summary("Scenario attachments service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity" / "attachments") + .out(statusCode(Ok).and(jsonBody[ScenarioAttachments].example(Examples.scenarioAttachments))) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + } + + val addAttachmentEndpoint: SecuredEndpoint[AddAttachmentRequest, ScenarioActivityError, Unit, Any] = { + baseNuApiEndpoint + .summary("Add scenario attachment service") + .tag("Activities") + .post + .in("processes" / path[ProcessName]("scenarioName") / path[VersionId]("versionId") / "activity" / "attachments") + .in(streamProvider.streamBodyEndpointInput) + .in(header[FileName](HeaderNames.ContentDisposition)) + .mapInTo[AddAttachmentRequest] + .out(statusCode(Ok)) + .errorOut(scenarioNotFoundErrorOutput) + .withSecurity(auth) + } + + val downloadAttachmentEndpoint + : SecuredEndpoint[GetAttachmentRequest, ScenarioActivityError, GetAttachmentResponse, Any] = { + baseNuApiEndpoint + .summary("Download attachment service") + .tag("Activities") + .get + .in("processes" / path[ProcessName]("scenarioName") / "activity" / "attachments" / path[Long]("attachmentId")) + .mapInTo[GetAttachmentRequest] + .out( + statusCode(Ok) + .and(streamProvider.streamBodyEndpointOutput) + .and(header(HeaderNames.ContentDisposition)(optionalHeaderCodec)) + .and(header(HeaderNames.ContentType)(requiredHeaderCodec)) + .mapTo[GetAttachmentResponse] + ) + .errorOut(scenarioNotFoundErrorOutput) + .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 new file mode 100644 index 00000000000..1050cd95c99 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Examples.scala @@ -0,0 +1,236 @@ +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._ +import sttp.tapir.EndpointIO.Example + +import java.time.Instant +import java.util.UUID + +object Examples { + + val deprecatedScenarioActivity: Example[Legacy.ProcessActivity] = Example.of( + summary = Some("Display scenario activity"), + value = Legacy.ProcessActivity( + comments = List( + Legacy.Comment( + id = 1L, + processVersionId = 1L, + content = "some comment", + user = "test", + createDate = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + attachments = List( + Legacy.Attachment( + id = 1L, + processVersionId = 1L, + fileName = "some_file.txt", + user = "test", + createDate = Instant.parse("2024-01-17T14:21:17Z") + ) + ) + ) + ) + + val scenarioActivities: Example[ScenarioActivities] = Example.of( + summary = Some("Display scenario actions"), + value = ScenarioActivities( + activities = List( + ScenarioActivity.ScenarioCreated( + id = UUID.fromString("80c95497-3b53-4435-b2d9-ae73c5766213"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + ), + ScenarioActivity.ScenarioArchived( + id = UUID.fromString("070a4e5c-21e5-4e63-acac-0052cf705a90"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + ), + ScenarioActivity.ScenarioUnarchived( + id = UUID.fromString("fa35d944-fe20-4c4f-96c6-316b6197951a"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + ), + ScenarioActivity.ScenarioDeployed( + id = UUID.fromString("545b7d87-8cdf-4cb5-92c4-38ddbfca3d08"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Deployment of scenario - task JIRA-1234"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + ScenarioActivity.ScenarioCanceled( + id = UUID.fromString("c354eba1-de97-455c-b977-74729c41ce7"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Canceled because marketing campaign ended"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + ScenarioActivity.ScenarioModified( + id = UUID.fromString("07b04d45-c7c0-4980-a3bc-3c7f66410f68"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Added new processing step"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + ScenarioActivity.ScenarioNameChanged( + id = UUID.fromString("da3d1f78-7d73-4ed9-b0e5-95538e150d0d"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + oldName = "marketing campaign", + newName = "old marketing campaign", + ), + ScenarioActivity.CommentAdded( + id = UUID.fromString("edf8b047-9165-445d-a173-ba61812dbd63"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Added new processing step"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ) + ), + ScenarioActivity.CommentAdded( + id = UUID.fromString("369367d6-d445-4327-ac23-4a94367b1d9e"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = None, + lastModifiedBy = "John Doe", + lastModifiedAt = Instant.parse("2024-01-18T14:21:17Z") + ) + ), + ScenarioActivity.AttachmentAdded( + id = UUID.fromString("b29916a9-34d4-4fc2-a6ab-79569f68c0b2"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + attachment = ScenarioActivityAttachment( + id = Some(10000001), + filename = "attachment01.png", + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ), + ), + ScenarioActivity.AttachmentAdded( + id = UUID.fromString("d0a7f4a2-abcc-4ffa-b1ca-68f6da3e999a"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + attachment = ScenarioActivityAttachment( + id = None, + filename = "attachment01.png", + lastModifiedBy = "John Doe", + lastModifiedAt = Instant.parse("2024-01-18T14:21:17Z") + ), + ), + ScenarioActivity.ChangedProcessingMode( + id = UUID.fromString("683df470-0b33-4ead-bf61-fa35c63484f3"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + from = "Request-Response", + to = "Batch", + ), + ScenarioActivity.IncomingMigration( + id = UUID.fromString("4da0f1ac-034a-49b6-81c9-8ee48ba1d830"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + sourceEnvironment = "preprod", + sourceScenarioVersion = "23", + ), + ScenarioActivity.OutgoingMigration( + id = UUID.fromString("49fcd45d-3fa6-48d4-b8ed-b3055910c7ad"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + comment = ScenarioActivityComment( + comment = Some("Added new processing step"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ), + destinationEnvironment = "preprod", + ), + ScenarioActivity.PerformedSingleExecution( + id = UUID.fromString("924dfcd3-fbc7-44ea-8763-813874382204"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + errorMessage = Some("Execution error occurred"), + ), + ScenarioActivity.PerformedSingleExecution( + id = UUID.fromString("924dfcd3-fbc7-44ea-8763-813874382204"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + errorMessage = None, + ), + ScenarioActivity.PerformedScheduledExecution( + id = UUID.fromString("9b27797e-aa03-42ba-8406-d0ae8005a883"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + errorMessage = None, + ), + ScenarioActivity.AutomaticUpdate( + id = UUID.fromString("33509d37-7657-4229-940f-b5736c82fb13"), + user = "some user", + date = Instant.parse("2024-01-17T14:21:17Z"), + scenarioVersion = Some(1), + dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + changes = "JIRA-12345, JIRA-32146", + errorMessage = None, + ), + ), + ) + ) + + val scenarioAttachments: Example[ScenarioAttachments] = Example.of( + summary = Some("Display scenario activity"), + value = ScenarioAttachments( + attachments = List( + Attachment( + id = 1L, + scenarioVersion = 1L, + fileName = "some_file.txt", + user = "test", + createDate = Instant.parse("2024-01-17T14:21:17Z") + ) + ) + ) + ) + + val noScenarioError: Example[NoScenario] = Example.of( + summary = Some("No scenario {scenarioName} found"), + value = NoScenario(ProcessName("'example scenario'")) + ) + + val commentNotFoundError: Example[NoComment] = Example.of( + summary = Some("Unable to edit comment with id: {commentId}"), + value = NoComment("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 new file mode 100644 index 00000000000..701f74bab3e --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/InputOutput.scala @@ -0,0 +1,30 @@ +package pl.touk.nussknacker.ui.api.description.scenarioActivity + +import pl.touk.nussknacker.engine.api.process.ProcessName +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError.NoScenario +import sttp.model.StatusCode.{NotFound, NotImplemented} +import sttp.tapir.EndpointIO.Example +import sttp.tapir.{EndpointOutput, emptyOutputAs, oneOf, oneOfVariantFromMatchType, plainBody} + +object InputOutput { + + val scenarioNotFoundErrorOutput: EndpointOutput.OneOf[ScenarioActivityError, ScenarioActivityError] = + oneOf[ScenarioActivityError]( + oneOfVariantFromMatchType( + NotFound, + plainBody[NoScenario] + .example( + Example.of( + summary = Some("No scenario {scenarioName} found"), + value = NoScenario(ProcessName("'example scenario'")) + ) + ) + ), + oneOfVariantFromMatchType( + NotImplemented, + emptyOutputAs(ScenarioActivityError.NotImplemented), + ) + ) + +} diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala index 49ade0644d3..7c0bc88ff69 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala @@ -1,5 +1,7 @@ package pl.touk.nussknacker.ui.api +import akka.actor.ActorSystem +import akka.stream.Materializer import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers import org.scalatest.prop.TableDrivenPropertyChecks._ @@ -8,6 +10,7 @@ import pl.touk.nussknacker.security.AuthCredentials.PassedAuthCredentials import pl.touk.nussknacker.test.utils.domain.ReflectionBasedUtils import pl.touk.nussknacker.test.utils.{InvalidExample, OpenAPIExamplesValidator, OpenAPISchemaComponents} import pl.touk.nussknacker.ui.security.api.AuthManager.ImpersonationConsideringInputEndpoint +import pl.touk.nussknacker.ui.server.{AkkaHttpBasedTapirStreamEndpointProvider, TapirStreamEndpointProvider} import pl.touk.nussknacker.ui.services.NuDesignerExposedApiHttpService import pl.touk.nussknacker.ui.util.Project import sttp.apispec.openapi.circe.yaml.RichOpenAPI @@ -15,6 +18,7 @@ import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter import sttp.tapir.{Endpoint, EndpointInput, auth} import java.lang.reflect.{Method, Modifier} +import scala.concurrent.Await import scala.util.Try // if the test fails it probably means that you should regenerate the Nu Designer OpenAPI document @@ -138,30 +142,46 @@ class NuDesignerApiAvailableToExposeYamlSpec extends AnyFunSuite with Matchers { object NuDesignerApiAvailableToExpose { - def generateOpenApiYaml: String = { - val endpoints = findApiEndpointsClasses().flatMap(findEndpointsInClass) + def generateOpenApiYaml: String = withStreamProvider { streamProvider => + val endpoints = findApiEndpointsClasses().flatMap(findEndpointsInClass(streamProvider)) val docs = OpenAPIDocsInterpreter(NuDesignerExposedApiHttpService.openAPIDocsOptions).toOpenAPI( es = endpoints, title = NuDesignerExposedApiHttpService.openApiDocumentTitle, version = "" ) - docs.toYaml } + private def withStreamProvider[T](handle: TapirStreamEndpointProvider => T): T = { + val actorSystem: ActorSystem = ActorSystem() + val mat: Materializer = Materializer(actorSystem) + val streamProvider: TapirStreamEndpointProvider = new AkkaHttpBasedTapirStreamEndpointProvider()(mat) + val result = handle(streamProvider) + Await.result(actorSystem.terminate(), scala.concurrent.duration.Duration.Inf) + result + } + private def findApiEndpointsClasses() = { ReflectionBasedUtils.findSubclassesOf[BaseEndpointDefinitions]("pl.touk.nussknacker.ui.api") } - private def findEndpointsInClass(clazz: Class[_ <: BaseEndpointDefinitions]) = { - val endpointDefinitions = createInstanceOf(clazz) + private def findEndpointsInClass( + streamEndpointProvider: TapirStreamEndpointProvider + )( + clazz: Class[_ <: BaseEndpointDefinitions] + ) = { + val endpointDefinitions = createInstanceOf(streamEndpointProvider)(clazz) clazz.getDeclaredMethods.toList .filter(isEndpointMethod) .sortBy(_.getName) .map(instantiateEndpointDefinition(endpointDefinitions, _)) } - private def createInstanceOf(clazz: Class[_ <: BaseEndpointDefinitions]) = { + private def createInstanceOf( + streamEndpointProvider: TapirStreamEndpointProvider, + )( + clazz: Class[_ <: BaseEndpointDefinitions], + ) = { val basicAuth = auth .basic[Option[String]]() .map(_.map(PassedAuthCredentials))(_.map(_.value)) @@ -173,6 +193,10 @@ object NuDesignerApiAvailableToExpose { Try(clazz.getDeclaredConstructor()) .map(_.newInstance()) } + .orElse { + Try(clazz.getConstructor(classOf[EndpointInput[PassedAuthCredentials]], classOf[TapirStreamEndpointProvider])) + .map(_.newInstance(basicAuth, streamEndpointProvider)) + } .getOrElse( throw new IllegalStateException( s"Class ${clazz.getName} is required to have either one parameter constructor or constructor without parameters" diff --git a/docs-internal/api/nu-designer-openapi.yaml b/docs-internal/api/nu-designer-openapi.yaml index 5e78df23cba..eeda77e9167 100644 --- a/docs-internal/api/nu-designer-openapi.yaml +++ b/docs-internal/api/nu-designer-openapi.yaml @@ -2249,12 +2249,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/{versionId}/activity/comments: - post: + /api/scenarioParametersCombinations: + get: tags: - - Scenario - summary: Add scenario comment service - operationId: postApiProcessesScenarionameVersionidActivityComments + - App + summary: Service providing available combinations of scenario's parameters + operationId: getApiScenarioparameterscombinations parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2263,29 +2263,32 @@ paths: type: - string - 'null' - - name: scenarioName - in: path - required: true - schema: - type: string - - name: versionId - in: path - required: true - schema: - type: integer - format: int64 - requestBody: - content: - text/plain: - schema: - type: string - required: true responses: '200': description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScenarioParametersCombinationWithEngineErrors' + examples: + Example: + summary: List of available parameters combinations + value: + combinations: + - processingMode: Unbounded-Stream + category: Marketing + engineSetupName: Flink + - processingMode: Request-Response + category: Fraud + engineSetupName: Lite K8s + - processingMode: Unbounded-Stream + category: Fraud + engineSetupName: Flink Fraud Detection + engineSetupErrors: + Flink: + - Invalid Flink configuration '400': - description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter versionId, Invalid value for: body' + description: 'Invalid value for: header Nu-Impersonate-User-Identity' content: text/plain: schema: @@ -2320,8 +2323,8 @@ paths: type: string examples: Example: - summary: No scenario {scenarioName} found - value: No scenario 'example scenario' found + summary: No impersonated user's data found for provided identity + value: No impersonated user data found for provided identity '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2336,12 +2339,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/comments/{commentId}: - delete: + /api/statistic: + post: tags: - - Scenario - summary: Delete process comment service - operationId: deleteApiProcessesScenarionameActivityCommentsCommentid + - Statistics + summary: Register statistics service + operationId: postApiStatistic parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2350,23 +2353,18 @@ paths: type: - string - 'null' - - name: scenarioName - in: path - required: true - schema: - type: string - - name: commentId - in: path + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterStatisticsRequestDto' required: true - schema: - type: integer - format: int64 responses: - '200': + '204': description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter commentId' + value for: body' content: text/plain: schema: @@ -2401,18 +2399,8 @@ paths: type: string examples: Example: - summary: No scenario {scenarioName} found - value: No scenario 'example scenario' found - '500': - description: '' - content: - text/plain: - schema: - type: string - examples: - Example: - summary: 'Unable to delete comment with id: {commentId}' - value: 'Unable to delete comment with id: 1' + summary: No impersonated user's data found for provided identity + value: No impersonated user data found for provided identity '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2427,12 +2415,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity: + /api/statistic/usage: get: tags: - - Scenario - summary: Scenario activity service - operationId: getApiProcessesScenarionameActivity + - Statistics + summary: Statistics URL service + operationId: getApiStatisticUsage parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2441,34 +2429,19 @@ paths: type: - string - 'null' - - name: scenarioName - in: path - required: true - schema: - type: string responses: '200': description: '' content: application/json: schema: - $ref: '#/components/schemas/ScenarioActivity' + $ref: '#/components/schemas/StatisticUrlResponseDto' examples: Example: - summary: Display scenario activity + summary: List of statistics URLs value: - comments: - - id: 1 - processVersionId: 1 - content: some comment - user: test - createDate: '2024-01-17T14:21:17Z' - attachments: - - id: 1 - processVersionId: 1 - fileName: some_file.txt - user: test - createDate: '2024-01-17T14:21:17Z' + urls: + - https://stats.nussknacker.io/?a_n=1&a_t=0&a_v=0&c=3&c_n=82&c_t=0&c_v=0&f_m=0&f_v=0&fingerprint=development&n_m=2&n_ma=0&n_mi=2&n_v=1&s_a=0&s_dm_c=1&s_dm_e=1&s_dm_f=2&s_dm_l=0&s_f=1&s_pm_b=0&s_pm_rr=1&s_pm_s=3&s_s=3&source=sources&u_ma=0&u_mi=0&u_v=0&v_m=2&v_ma=1&v_mi=3&v_v=2&version=1.15.0-SNAPSHOT '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity' content: @@ -2505,8 +2478,18 @@ paths: type: string examples: Example: - summary: No scenario {scenarioName} found - value: No scenario 'example scenario' found + summary: No impersonated user's data found for provided identity + value: No impersonated user data found for provided identity + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Statistics generation failed. + value: Statistics generation failed. '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2521,12 +2504,12 @@ paths: security: - {} - httpAuth: [] - /api/scenarioParametersCombinations: + /api/user: get: tags: - - App - summary: Service providing available combinations of scenario's parameters - operationId: getApiScenarioparameterscombinations + - User + summary: Logged user info service + operationId: getApiUser parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2541,24 +2524,31 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ScenarioParametersCombinationWithEngineErrors' + $ref: '#/components/schemas/DisplayableUser' examples: - Example: - summary: List of available parameters combinations + Example0: + summary: Common user info value: - combinations: - - processingMode: Unbounded-Stream - category: Marketing - engineSetupName: Flink - - processingMode: Request-Response - category: Fraud - engineSetupName: Lite K8s - - processingMode: Unbounded-Stream - category: Fraud - engineSetupName: Flink Fraud Detection - engineSetupErrors: - Flink: - - Invalid Flink configuration + id: reader + username: reader + isAdmin: false + categories: + - Category1 + categoryPermissions: + Category1: + - Read + globalPermissions: [] + Example1: + summary: Admin user info + value: + id: admin + username: admin + isAdmin: true + categories: + - Category1 + - Category2 + categoryPermissions: {} + globalPermissions: [] '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity' content: @@ -2611,12 +2601,12 @@ paths: security: - {} - httpAuth: [] - /api/statistic: + /api/processes/{scenarioName}/{versionId}/activity/attachments: post: tags: - - Statistics - summary: Register statistics service - operationId: postApiStatistic + - Activities + summary: Add scenario attachment service + operationId: postApiProcessesScenarionameVersionidActivityAttachments parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2625,18 +2615,36 @@ paths: type: - string - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: versionId + in: path + required: true + schema: + type: integer + format: int64 + - name: Content-Disposition + in: header + required: true + schema: + type: string requestBody: content: - application/json: + application/octet-stream: schema: - $ref: '#/components/schemas/RegisterStatisticsRequestDto' + type: string + format: binary required: true responses: - '204': + '200': description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: body' + value for: path parameter versionId, Invalid value for: body, Invalid + value for: header Content-Disposition' content: text/plain: schema: @@ -2671,8 +2679,8 @@ paths: type: string examples: Example: - summary: No impersonated user's data found for provided identity - value: No impersonated user data found for provided identity + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2687,12 +2695,12 @@ paths: security: - {} - httpAuth: [] - /api/statistic/usage: - get: + /api/processes/{scenarioName}/{versionId}/activity/comment: + post: tags: - - Statistics - summary: Statistics URL service - operationId: getApiStatisticUsage + - Activities + summary: Add scenario comment service + operationId: postApiProcessesScenarionameVersionidActivityComment parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2701,21 +2709,29 @@ paths: type: - string - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: versionId + in: path + required: true + schema: + type: integer + format: int64 + requestBody: + content: + text/plain: + schema: + type: string + required: true responses: '200': description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/StatisticUrlResponseDto' - examples: - Example: - summary: List of statistics URLs - value: - urls: - - https://stats.nussknacker.io/?a_n=1&a_t=0&a_v=0&c=3&c_n=82&c_t=0&c_v=0&f_m=0&f_v=0&fingerprint=development&n_m=2&n_ma=0&n_mi=2&n_v=1&s_a=0&s_dm_c=1&s_dm_e=1&s_dm_f=2&s_dm_l=0&s_f=1&s_pm_b=0&s_pm_rr=1&s_pm_s=3&s_s=3&source=sources&u_ma=0&u_mi=0&u_v=0&v_m=2&v_ma=1&v_mi=3&v_v=2&version=1.15.0-SNAPSHOT '400': - description: 'Invalid value for: header Nu-Impersonate-User-Identity' + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter versionId, Invalid value for: body' content: text/plain: schema: @@ -2750,18 +2766,873 @@ paths: type: string examples: Example: - summary: No impersonated user's data found for provided identity - value: No impersonated user data found for provided identity - '500': - description: '' - content: - text/plain: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/attachments: + get: + tags: + - Activities + summary: Scenario attachments service + operationId: getApiProcessesScenarionameActivityAttachments + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScenarioAttachments' + examples: + Example: + summary: Display scenario activity + value: + attachments: + - id: 1 + scenarioVersion: 1 + fileName: some_file.txt + user: test + createDate: '2024-01-17T14:21:17Z' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/comment/{scenarioActivityId}: + put: + tags: + - Activities + summary: Edit process comment service + operationId: putApiProcessesScenarionameActivityCommentScenarioactivityid + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: scenarioActivityId + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + text/plain: + schema: + type: string + required: true + responses: + '200': + description: '' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter scenarioActivityId, Invalid value for: body' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + delete: + tags: + - Activities + summary: Delete process comment service + operationId: deleteApiProcessesScenarionameActivityCommentScenarioactivityid + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: scenarioActivityId + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: '' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter scenarioActivityId' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/{versionId}/activity/comments: + post: + tags: + - Activities + summary: Add scenario comment service + operationId: postApiProcessesScenarionameVersionidActivityComments + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: versionId + in: path + required: true + schema: + type: integer + format: int64 + requestBody: + content: + text/plain: + schema: + type: string + required: true + responses: + '200': + description: '' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter versionId, Invalid value for: body' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + deprecated: true + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/comments/{commentId}: + delete: + tags: + - Activities + summary: Delete process comment service + operationId: deleteApiProcessesScenarionameActivityCommentsCommentid + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: commentId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: '' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter commentId' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + deprecated: true + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity: + get: + tags: + - Activities + summary: Scenario activity service + operationId: getApiProcessesScenarionameActivity + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessActivity' + examples: + Example: + summary: Display scenario activity + value: + comments: + - id: 1 + processVersionId: 1 + content: some comment + user: test + createDate: '2024-01-17T14:21:17Z' + attachments: + - id: 1 + processVersionId: 1 + fileName: some_file.txt + user: test + createDate: '2024-01-17T14:21:17Z' + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + deprecated: true + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/attachments/{attachmentId}: + get: + tags: + - Activities + summary: Download attachment service + operationId: getApiProcessesScenarionameActivityAttachmentsAttachmentid + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + - name: attachmentId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: '' + headers: + Content-Disposition: + required: false + schema: + type: + - string + - 'null' + Content-Type: + required: true + schema: + type: string + content: + application/octet-stream: + schema: + type: string + format: binary + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter attachmentId' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: + schema: + type: string + examples: + Example: + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found + '501': + description: Impersonation is not supported for defined authentication mechanism + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Cannot authenticate impersonated user as impersonation + is not supported by the authentication mechanism + value: Provided authentication method does not support impersonation + security: + - {} + - httpAuth: [] + /api/processes/{scenarioName}/activity/activities: + get: + tags: + - Activities + summary: Scenario activities service + operationId: getApiProcessesScenarionameActivityActivities + parameters: + - name: Nu-Impersonate-User-Identity + in: header + required: false + schema: + type: + - string + - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScenarioActivities' + examples: + Example: + summary: Display scenario actions + value: + activities: + - id: 80c95497-3b53-4435-b2d9-ae73c5766213 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + type: SCENARIO_CREATED + - id: 070a4e5c-21e5-4e63-acac-0052cf705a90 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + type: SCENARIO_ARCHIVED + - id: fa35d944-fe20-4c4f-96c6-316b6197951a + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + type: SCENARIO_UNARCHIVED + - id: 545b7d87-8cdf-4cb5-92c4-38ddbfca3d08 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Deployment of scenario - task JIRA-1234 + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: SCENARIO_DEPLOYED + - id: c354eba1-de97-455c-b977-074729c41ce7 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Canceled because marketing campaign ended + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: SCENARIO_CANCELED + - id: 07b04d45-c7c0-4980-a3bc-3c7f66410f68 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Added new processing step + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: SCENARIO_MODIFIED + - id: da3d1f78-7d73-4ed9-b0e5-95538e150d0d + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + oldName: marketing campaign + newName: old marketing campaign + type: SCENARIO_NAME_CHANGED + - id: edf8b047-9165-445d-a173-ba61812dbd63 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Added new processing step + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: COMMENT_ADDED + - id: 369367d6-d445-4327-ac23-4a94367b1d9e + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + lastModifiedBy: John Doe + lastModifiedAt: '2024-01-18T14:21:17Z' + type: COMMENT_ADDED + - id: b29916a9-34d4-4fc2-a6ab-79569f68c0b2 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + attachment: + id: 10000001 + filename: attachment01.png + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + type: ATTACHMENT_ADDED + - id: d0a7f4a2-abcc-4ffa-b1ca-68f6da3e999a + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + attachment: + filename: attachment01.png + lastModifiedBy: John Doe + lastModifiedAt: '2024-01-18T14:21:17Z' + type: ATTACHMENT_ADDED + - id: 683df470-0b33-4ead-bf61-fa35c63484f3 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + from: Request-Response + to: Batch + type: CHANGED_PROCESSING_MODE + - id: 4da0f1ac-034a-49b6-81c9-8ee48ba1d830 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + sourceEnvironment: preprod + sourceScenarioVersion: '23' + type: INCOMING_MIGRATION + - id: 49fcd45d-3fa6-48d4-b8ed-b3055910c7ad + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + comment: + comment: Added new processing step + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' + destinationEnvironment: preprod + type: OUTGOING_MIGRATION + - id: 924dfcd3-fbc7-44ea-8763-813874382204 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + dateFinished: '2024-01-17T14:21:17Z' + errorMessage: Execution error occurred + type: PERFORMED_SINGLE_EXECUTION + - id: 924dfcd3-fbc7-44ea-8763-813874382204 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + dateFinished: '2024-01-17T14:21:17Z' + type: PERFORMED_SINGLE_EXECUTION + - id: 9b27797e-aa03-42ba-8406-d0ae8005a883 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + dateFinished: '2024-01-17T14:21:17Z' + type: PERFORMED_SCHEDULED_EXECUTION + - id: 33509d37-7657-4229-940f-b5736c82fb13 + user: some user + date: '2024-01-17T14:21:17Z' + scenarioVersion: 1 + dateFinished: '2024-01-17T14:21:17Z' + changes: JIRA-12345, JIRA-32146 + type: AUTOMATIC_UPDATE + '400': + description: 'Invalid value for: header Nu-Impersonate-User-Identity' + content: + text/plain: + schema: + type: string + '401': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authentication failed + value: The supplied authentication is invalid + '403': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: Authorization failed + value: The supplied authentication is not authorized to access this + resource + '404': + description: Identity provided in the Nu-Impersonate-User-Identity header + did not match any user + content: + text/plain: schema: type: string examples: Example: - summary: Statistics generation failed. - value: Statistics generation failed. + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2776,12 +3647,12 @@ paths: security: - {} - httpAuth: [] - /api/user: + /api/processes/{scenarioName}/activity/activities/metadata: get: tags: - - User - summary: Logged user info service - operationId: getApiUser + - Activities + summary: Scenario activities metadata service + operationId: getApiProcessesScenarionameActivityActivitiesMetadata parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2790,37 +3661,109 @@ paths: type: - string - 'null' + - name: scenarioName + in: path + required: true + schema: + type: string responses: '200': description: '' content: application/json: schema: - $ref: '#/components/schemas/DisplayableUser' - examples: - Example0: - summary: Common user info - value: - id: reader - username: reader - isAdmin: false - categories: - - Category1 - categoryPermissions: - Category1: - - Read - globalPermissions: [] - Example1: - summary: Admin user info - value: - id: admin - username: admin - isAdmin: true - categories: - - Category1 - - Category2 - categoryPermissions: {} - globalPermissions: [] + $ref: '#/components/schemas/ScenarioActivitiesMetadata' + example: + activities: + - type: SCENARIO_CREATED + displayableName: Scenario created + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_ARCHIVED + displayableName: Scenario archived + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_UNARCHIVED + displayableName: Scenario unarchived + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_DEPLOYED + displayableName: Deployment + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_PAUSED + displayableName: Pause + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_CANCELED + displayableName: Cancel + icon: /assets/states/error.svg + supportedActions: [] + - type: SCENARIO_MODIFIED + displayableName: New version saved + icon: /assets/states/error.svg + supportedActions: + - compare + - type: SCENARIO_NAME_CHANGED + displayableName: Scenario name changed + icon: /assets/states/error.svg + supportedActions: [] + - type: COMMENT_ADDED + displayableName: Comment + icon: /assets/states/error.svg + supportedActions: + - delete_comment + - edit_comment + - type: ATTACHMENT_ADDED + displayableName: Attachment + icon: /assets/states/error.svg + supportedActions: [] + - type: CHANGED_PROCESSING_MODE + displayableName: Processing mode change + icon: /assets/states/error.svg + supportedActions: [] + - type: INCOMING_MIGRATION + displayableName: Incoming migration + icon: /assets/states/error.svg + supportedActions: + - compare + - type: OUTGOING_MIGRATION + displayableName: Outgoing migration + icon: /assets/states/error.svg + supportedActions: [] + - type: PERFORMED_SINGLE_EXECUTION + displayableName: Processing data + icon: /assets/states/error.svg + supportedActions: [] + - type: PERFORMED_SCHEDULED_EXECUTION + displayableName: Processing data + icon: /assets/states/error.svg + supportedActions: [] + - type: AUTOMATIC_UPDATE + displayableName: Automatic update + icon: /assets/states/error.svg + supportedActions: + - compare + - type: CUSTOM_ACTION + displayableName: Custom action + icon: /assets/states/error.svg + supportedActions: [] + actions: + - id: compare + displayableName: Compare + icon: /assets/states/error.svg + - id: delete_comment + displayableName: Delete + icon: /assets/states/error.svg + - id: edit_comment + displayableName: Edit + icon: /assets/states/error.svg + - id: download_attachment + displayableName: Download + icon: /assets/states/error.svg + - id: delete_attachment + displayableName: Delete + icon: /assets/states/error.svg '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity' content: @@ -2857,8 +3800,8 @@ paths: type: string examples: Example: - summary: No impersonated user's data found for provided identity - value: No impersonated user data found for provided identity + summary: No scenario {scenarioName} found + value: No scenario 'example scenario' found '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2889,6 +3832,29 @@ components: type: integer format: int32 Attachment: + title: Attachment + type: object + required: + - id + - scenarioVersion + - fileName + - user + - createDate + properties: + id: + type: integer + format: int64 + scenarioVersion: + type: integer + format: int64 + fileName: + type: string + user: + type: string + createDate: + type: string + format: date-time + Attachment1: title: Attachment type: object required: @@ -2911,6 +3877,68 @@ components: createDate: type: string format: date-time + AttachmentAdded: + title: AttachmentAdded + type: object + required: + - id + - user + - date + - attachment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + attachment: + $ref: '#/components/schemas/ScenarioActivityAttachment' + type: + type: string + AutomaticUpdate: + title: AutomaticUpdate + type: object + required: + - id + - user + - date + - dateFinished + - changes + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + dateFinished: + type: string + format: date-time + changes: + type: string + errorMessage: + type: + - string + - 'null' + type: + type: string BoolParameterEditor: title: BoolParameterEditor type: object @@ -2989,6 +4017,36 @@ components: format: int32 errorMessage: type: string + ChangedProcessingMode: + title: ChangedProcessingMode + type: object + required: + - id + - user + - date + - from + - to + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + from: + type: string + to: + type: string + type: + type: string ColumnDefinition: title: ColumnDefinition type: object @@ -3023,6 +4081,33 @@ components: createDate: type: string format: date-time + CommentAdded: + title: CommentAdded + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string ComponentLink: title: ComponentLink type: object @@ -3130,6 +4215,33 @@ components: CronParameterEditor: title: CronParameterEditor type: object + CustomAction: + title: CustomAction + type: object + required: + - id + - user + - date + - actionName + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + actionName: + type: string + type: + type: string CustomActionRequest: title: CustomActionRequest type: object @@ -3545,6 +4657,36 @@ components: uniqueItems: true items: type: string + IncomingMigration: + title: IncomingMigration + type: object + required: + - id + - user + - date + - sourceEnvironment + - sourceScenarioVersion + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + sourceEnvironment: + type: string + sourceScenarioVersion: + type: string + type: + type: string JsonParameterEditor: title: JsonParameterEditor type: object @@ -4186,6 +5328,36 @@ components: - info - success - error + OutgoingMigration: + title: OutgoingMigration + type: object + required: + - id + - user + - date + - comment + - destinationEnvironment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + destinationEnvironment: + type: string + type: + type: string Parameter: title: Parameter type: object @@ -4270,14 +5442,78 @@ components: title: ParametersValidationResultDto type: object required: - - validationPerformed + - validationPerformed + properties: + validationErrors: + type: array + items: + $ref: '#/components/schemas/NodeValidationError' + validationPerformed: + type: boolean + PerformedScheduledExecution: + title: PerformedScheduledExecution + type: object + required: + - id + - user + - date + - dateFinished + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + dateFinished: + type: string + format: date-time + errorMessage: + type: + - string + - 'null' + type: + type: string + PerformedSingleExecution: + title: PerformedSingleExecution + type: object + required: + - id + - user + - date + - dateFinished + - type properties: - validationErrors: - type: array - items: - $ref: '#/components/schemas/NodeValidationError' - validationPerformed: - type: boolean + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + dateFinished: + type: string + format: date-time + errorMessage: + type: + - string + - 'null' + type: + type: string PeriodParameterEditor: title: PeriodParameterEditor type: object @@ -4352,6 +5588,18 @@ components: - FINISHED - FAILED - EXECUTION_FINISHED + ProcessActivity: + title: ProcessActivity + type: object + properties: + comments: + type: array + items: + $ref: '#/components/schemas/Comment' + attachments: + type: array + items: + $ref: '#/components/schemas/Attachment1' ProcessAdditionalFields: title: ProcessAdditionalFields type: object @@ -4415,18 +5663,301 @@ components: type: - string - 'null' + ScenarioActivities: + title: ScenarioActivities + type: object + properties: + activities: + type: array + items: + $ref: '#/components/schemas/ScenarioActivity' + ScenarioActivitiesMetadata: + title: ScenarioActivitiesMetadata + type: object + properties: + activities: + type: array + items: + $ref: '#/components/schemas/ScenarioActivityMetadata' + actions: + type: array + items: + $ref: '#/components/schemas/ScenarioActivityActionMetadata' ScenarioActivity: title: ScenarioActivity + oneOf: + - $ref: '#/components/schemas/AttachmentAdded' + - $ref: '#/components/schemas/AutomaticUpdate' + - $ref: '#/components/schemas/ChangedProcessingMode' + - $ref: '#/components/schemas/CommentAdded' + - $ref: '#/components/schemas/CustomAction' + - $ref: '#/components/schemas/IncomingMigration' + - $ref: '#/components/schemas/OutgoingMigration' + - $ref: '#/components/schemas/PerformedScheduledExecution' + - $ref: '#/components/schemas/PerformedSingleExecution' + - $ref: '#/components/schemas/ScenarioArchived' + - $ref: '#/components/schemas/ScenarioCanceled' + - $ref: '#/components/schemas/ScenarioCreated' + - $ref: '#/components/schemas/ScenarioDeployed' + - $ref: '#/components/schemas/ScenarioModified' + - $ref: '#/components/schemas/ScenarioNameChanged' + - $ref: '#/components/schemas/ScenarioPaused' + - $ref: '#/components/schemas/ScenarioUnarchived' + discriminator: + propertyName: type + mapping: + ATTACHMENT_ADDED: '#/components/schemas/AttachmentAdded' + AUTOMATIC_UPDATE: '#/components/schemas/AutomaticUpdate' + CHANGED_PROCESSING_MODE: '#/components/schemas/ChangedProcessingMode' + COMMENT_ADDED: '#/components/schemas/CommentAdded' + CUSTOM_ACTION: '#/components/schemas/CustomAction' + INCOMING_MIGRATION: '#/components/schemas/IncomingMigration' + OUTGOING_MIGRATION: '#/components/schemas/OutgoingMigration' + PERFORMED_SCHEDULED_EXECUTION: '#/components/schemas/PerformedScheduledExecution' + PERFORMED_SINGLE_EXECUTION: '#/components/schemas/PerformedSingleExecution' + SCENARIO_ARCHIVED: '#/components/schemas/ScenarioArchived' + SCENARIO_CANCELED: '#/components/schemas/ScenarioCanceled' + SCENARIO_CREATED: '#/components/schemas/ScenarioCreated' + SCENARIO_DEPLOYED: '#/components/schemas/ScenarioDeployed' + SCENARIO_MODIFIED: '#/components/schemas/ScenarioModified' + SCENARIO_NAME_CHANGED: '#/components/schemas/ScenarioNameChanged' + SCENARIO_PAUSED: '#/components/schemas/ScenarioPaused' + SCENARIO_UNARCHIVED: '#/components/schemas/ScenarioUnarchived' + ScenarioActivityActionMetadata: + title: ScenarioActivityActionMetadata type: object + required: + - id + - displayableName + - icon properties: - comments: + id: + type: string + displayableName: + type: string + icon: + type: string + ScenarioActivityAttachment: + title: ScenarioActivityAttachment + type: object + required: + - filename + - lastModifiedBy + - lastModifiedAt + properties: + id: + type: + - integer + - 'null' + format: int64 + filename: + type: string + lastModifiedBy: + type: string + lastModifiedAt: + type: string + format: date-time + ScenarioActivityComment: + title: ScenarioActivityComment + type: object + required: + - lastModifiedBy + - lastModifiedAt + properties: + comment: + type: + - string + - 'null' + lastModifiedBy: + type: string + lastModifiedAt: + type: string + format: date-time + ScenarioActivityMetadata: + title: ScenarioActivityMetadata + type: object + required: + - type + - displayableName + - icon + properties: + type: + type: string + displayableName: + type: string + icon: + type: string + supportedActions: type: array items: - $ref: '#/components/schemas/Comment' + type: string + ScenarioArchived: + title: ScenarioArchived + type: object + required: + - id + - user + - date + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + type: + type: string + ScenarioAttachments: + title: ScenarioAttachments + type: object + properties: attachments: type: array items: $ref: '#/components/schemas/Attachment' + ScenarioCanceled: + title: ScenarioCanceled + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string + ScenarioCreated: + title: ScenarioCreated + type: object + required: + - id + - user + - date + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + type: + type: string + ScenarioDeployed: + title: ScenarioDeployed + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string + ScenarioModified: + title: ScenarioModified + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string + ScenarioNameChanged: + title: ScenarioNameChanged + type: object + required: + - id + - user + - date + - oldName + - newName + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + oldName: + type: string + newName: + type: string + type: + type: string ScenarioParameters: title: ScenarioParameters type: object @@ -4453,6 +5984,57 @@ components: $ref: '#/components/schemas/ScenarioParameters' engineSetupErrors: $ref: '#/components/schemas/Map_EngineSetupName_List_String' + ScenarioPaused: + title: ScenarioPaused + type: object + required: + - id + - user + - date + - comment + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' + type: + type: string + ScenarioUnarchived: + title: ScenarioUnarchived + type: object + required: + - id + - user + - date + - type + properties: + id: + type: string + format: uuid + user: + type: string + date: + type: string + format: date-time + scenarioVersion: + type: + - integer + - 'null' + format: int64 + type: + type: string ScenarioUsageData: title: ScenarioUsageData type: object From 1ed303c757788f9a3c7de94454248f367636bc0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 9 Sep 2024 15:04:10 +0200 Subject: [PATCH 24/43] qs --- .../api/ScenarioActivityApiHttpService.scala | 78 +++++++++---------- 1 file changed, 39 insertions(+), 39 deletions(-) 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 486ad53dd3b..062c88c1f41 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 @@ -93,6 +93,31 @@ class ScenarioActivityApiHttpService( } } + expose { + endpoints.addAttachmentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: AddAttachmentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Write) + _ <- saveAttachment(request, scenarioId) + } yield () + } + } + + expose { + endpoints.downloadAttachmentEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => request: GetAttachmentRequest => + for { + scenarioId <- getScenarioIdByName(request.scenarioName) + _ <- isAuthorized(scenarioId, Permission.Read) + maybeAttachment <- EitherT.right(attachmentService.readAttachment(request.attachmentId, scenarioId)) + response = buildResponse(maybeAttachment) + } yield response + } + } + expose { endpoints.scenarioActivitiesEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) @@ -100,11 +125,23 @@ class ScenarioActivityApiHttpService( for { scenarioId <- getScenarioIdByName(scenarioName) _ <- isAuthorized(scenarioId, Permission.Read) - activities <- EitherT.liftF(Future.failed(new Exception("API not yet implemented"))) + activities <- notImplemented[List[ScenarioActivity]] } yield ScenarioActivities(activities) } } + expose { + endpoints.attachmentsEndpoint + .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) + .serverLogicEitherT { implicit loggedUser => processName: ProcessName => + for { + scenarioId <- getScenarioIdByName(processName) + _ <- isAuthorized(scenarioId, Permission.Read) + attachments <- notImplemented[ScenarioAttachments] + } yield attachments + } + } + expose { endpoints.scenarioActivitiesMetadataEndpoint .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) @@ -124,7 +161,7 @@ class ScenarioActivityApiHttpService( for { scenarioId <- getScenarioIdByName(request.scenarioName) _ <- isAuthorized(scenarioId, Permission.Write) - _ <- addNewComment(request, scenarioId) + _ <- notImplemented[Unit] } yield () } } @@ -153,43 +190,6 @@ class ScenarioActivityApiHttpService( } } - expose { - endpoints.attachmentsEndpoint - .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) - .serverLogicEitherT { implicit loggedUser => processName: ProcessName => - for { - scenarioId <- getScenarioIdByName(processName) - _ <- isAuthorized(scenarioId, Permission.Read) - attachments <- notImplemented[ScenarioAttachments] - } yield attachments - } - } - - expose { - endpoints.addAttachmentEndpoint - .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) - .serverLogicEitherT { implicit loggedUser => request: AddAttachmentRequest => - for { - scenarioId <- getScenarioIdByName(request.scenarioName) - _ <- isAuthorized(scenarioId, Permission.Write) - _ <- saveAttachment(request, scenarioId) - } yield () - } - } - - expose { - endpoints.downloadAttachmentEndpoint - .serverSecurityLogic(authorizeKnownUser[ScenarioActivityError]) - .serverLogicEitherT { implicit loggedUser => request: GetAttachmentRequest => - for { - scenarioId <- getScenarioIdByName(request.scenarioName) - _ <- isAuthorized(scenarioId, Permission.Read) - maybeAttachment <- EitherT.right(attachmentService.readAttachment(request.attachmentId, scenarioId)) - response = buildResponse(maybeAttachment) - } yield response - } - } - private def notImplemented[T]: EitherT[Future, ScenarioActivityError, T] = EitherT.leftT[Future, T](ScenarioActivityError.NotImplemented: ScenarioActivityError) From 379ecd4028b1db0e4da7d1d958e12e8b25cf06bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 9 Sep 2024 16:10:10 +0200 Subject: [PATCH 25/43] qs --- ...DesignerApiAvailableToExposeYamlSpec.scala | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala index 7c0bc88ff69..dc1ee5124c6 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala @@ -27,22 +27,23 @@ import scala.util.Try // Warning! OpenAPI can be generated differently depending on the scala version. class NuDesignerApiAvailableToExposeYamlSpec extends AnyFunSuite with Matchers { - test("Nu Designer OpenAPI document with all available to expose endpoints should have examples matching schemas") { - val generatedSpec = NuDesignerApiAvailableToExpose.generateOpenApiYaml - val examplesValidationResult = OpenAPIExamplesValidator.forTapir.validateExamples(generatedSpec) - val clue = examplesValidationResult - .map { case InvalidExample(_, _, operationId, isRequest, exampleId, errors) => - errors - .map(_.getMessage) - .distinct - .map(" " + _) - .mkString(s"$operationId > ${if (isRequest) "request" else "response"} > $exampleId\n", "\n", "") - } - .mkString("", "\n", "\n") - withClue(clue) { - examplesValidationResult.size shouldEqual 0 - } - } +// todo NU-1772: the JSON schema validation does not correctly handle the responses with discriminator +// test("Nu Designer OpenAPI document with all available to expose endpoints should have examples matching schemas") { +// val generatedSpec = NuDesignerApiAvailableToExpose.generateOpenApiYaml +// val examplesValidationResult = OpenAPIExamplesValidator.forTapir.validateExamples(generatedSpec) +// val clue = examplesValidationResult +// .map { case InvalidExample(_, _, operationId, isRequest, exampleId, errors) => +// errors +// .map(_.getMessage) +// .distinct +// .map(" " + _) +// .mkString(s"$operationId > ${if (isRequest) "request" else "response"} > $exampleId\n", "\n", "") +// } +// .mkString("", "\n", "\n") +// withClue(clue) { +// examplesValidationResult.size shouldEqual 0 +// } +// } test("Nu Designer OpenAPI document with all available to expose endpoints has to be up to date") { val currentNuDesignerOpenApiYamlContent = From 36c721ccdc2512ea66aad75d389a322477746329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 10 Sep 2024 15:14:22 +0200 Subject: [PATCH 26/43] qs --- .../test/utils/OpenAPIExamplesValidator.scala | 9 +- ...DesignerApiAvailableToExposeYamlSpec.scala | 38 +-- docs-internal/api/nu-designer-openapi.yaml | 216 +++++++++--------- 3 files changed, 136 insertions(+), 127 deletions(-) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/OpenAPIExamplesValidator.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/OpenAPIExamplesValidator.scala index c5e2a0be450..bd77d50a291 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/OpenAPIExamplesValidator.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/OpenAPIExamplesValidator.scala @@ -13,7 +13,10 @@ class OpenAPIExamplesValidator private (schemaFactory: JsonSchemaFactory) { import OpenAPIExamplesValidator._ - def validateExamples(specYaml: String): List[InvalidExample] = { + def validateExamples( + specYaml: String, + excludeResponseValidationForOperationIds: List[String] = List.empty + ): List[InvalidExample] = { val specJson = YamlParser.parse(specYaml).toOption.get val componentsSchemas = specJson.hcursor @@ -26,7 +29,9 @@ class OpenAPIExamplesValidator private (schemaFactory: JsonSchemaFactory) { (_, operation) <- pathItem.asObject.map(_.toList).getOrElse(List.empty) operationId = operation.hcursor.downField("operationId").as[String].toTry.get invalidExample <- validateRequestExample(operation, operationId, componentsSchemas) ::: - validateResponsesExamples(operation, operationId, componentsSchemas) + (if (!excludeResponseValidationForOperationIds.contains(operationId)) + validateResponsesExamples(operation, operationId, componentsSchemas) + else List.empty) } yield invalidExample } diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala index dc1ee5124c6..04dcefdfdf7 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala @@ -27,23 +27,27 @@ import scala.util.Try // Warning! OpenAPI can be generated differently depending on the scala version. class NuDesignerApiAvailableToExposeYamlSpec extends AnyFunSuite with Matchers { -// todo NU-1772: the JSON schema validation does not correctly handle the responses with discriminator -// test("Nu Designer OpenAPI document with all available to expose endpoints should have examples matching schemas") { -// val generatedSpec = NuDesignerApiAvailableToExpose.generateOpenApiYaml -// val examplesValidationResult = OpenAPIExamplesValidator.forTapir.validateExamples(generatedSpec) -// val clue = examplesValidationResult -// .map { case InvalidExample(_, _, operationId, isRequest, exampleId, errors) => -// errors -// .map(_.getMessage) -// .distinct -// .map(" " + _) -// .mkString(s"$operationId > ${if (isRequest) "request" else "response"} > $exampleId\n", "\n", "") -// } -// .mkString("", "\n", "\n") -// withClue(clue) { -// examplesValidationResult.size shouldEqual 0 -// } -// } + test("Nu Designer OpenAPI document with all available to expose endpoints should have examples matching schemas") { + val generatedSpec = NuDesignerApiAvailableToExpose.generateOpenApiYaml + val examplesValidationResult = OpenAPIExamplesValidator.forTapir.validateExamples( + specYaml = generatedSpec, + excludeResponseValidationForOperationIds = List( + "getApiProcessesScenarionameActivityActivities" // todo NU-1772: responses contains discriminator, it is not properly handled by validator + ) + ) + val clue = examplesValidationResult + .map { case InvalidExample(_, _, operationId, isRequest, exampleId, errors) => + errors + .map(_.getMessage) + .distinct + .map(" " + _) + .mkString(s"$operationId > ${if (isRequest) "request" else "response"} > $exampleId\n", "\n", "") + } + .mkString("", "\n", "\n") + withClue(clue) { + examplesValidationResult.size shouldEqual 0 + } + } test("Nu Designer OpenAPI document with all available to expose endpoints has to be up to date") { val currentNuDesignerOpenApiYamlContent = diff --git a/docs-internal/api/nu-designer-openapi.yaml b/docs-internal/api/nu-designer-openapi.yaml index eeda77e9167..771bdf5f7db 100644 --- a/docs-internal/api/nu-designer-openapi.yaml +++ b/docs-internal/api/nu-designer-openapi.yaml @@ -2870,12 +2870,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/comment/{scenarioActivityId}: - put: + /api/processes/{scenarioName}/{versionId}/activity/comments: + post: tags: - Activities - summary: Edit process comment service - operationId: putApiProcessesScenarionameActivityCommentScenarioactivityid + summary: Add scenario comment service + operationId: postApiProcessesScenarionameVersionidActivityComments parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2889,12 +2889,12 @@ paths: required: true schema: type: string - - name: scenarioActivityId + - name: versionId in: path required: true schema: - type: string - format: uuid + type: integer + format: int64 requestBody: content: text/plain: @@ -2906,7 +2906,7 @@ paths: description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter scenarioActivityId, Invalid value for: body' + value for: path parameter versionId, Invalid value for: body' content: text/plain: schema: @@ -2943,16 +2943,6 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found - '500': - description: '' - content: - text/plain: - schema: - type: string - examples: - Example: - summary: 'Unable to edit comment with id: {commentId}' - value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2964,14 +2954,16 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation + deprecated: true security: - {} - httpAuth: [] + /api/processes/{scenarioName}/activity/comments/{commentId}: delete: tags: - Activities summary: Delete process comment service - operationId: deleteApiProcessesScenarionameActivityCommentScenarioactivityid + operationId: deleteApiProcessesScenarionameActivityCommentsCommentid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2985,18 +2977,18 @@ paths: required: true schema: type: string - - name: scenarioActivityId + - name: commentId in: path required: true schema: - type: string - format: uuid + type: integer + format: int64 responses: '200': description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter scenarioActivityId' + value for: path parameter commentId' content: text/plain: schema: @@ -3054,15 +3046,16 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation + deprecated: true security: - {} - httpAuth: [] - /api/processes/{scenarioName}/{versionId}/activity/comments: - post: + /api/processes/{scenarioName}/activity: + get: tags: - Activities - summary: Add scenario comment service - operationId: postApiProcessesScenarionameVersionidActivityComments + summary: Scenario activity service + operationId: getApiProcessesScenarionameActivity parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3076,24 +3069,31 @@ paths: required: true schema: type: string - - name: versionId - in: path - required: true - schema: - type: integer - format: int64 - requestBody: - content: - text/plain: - schema: - type: string - required: true responses: '200': description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessActivity' + examples: + Example: + summary: Display scenario activity + value: + comments: + - id: 1 + processVersionId: 1 + content: some comment + user: test + createDate: '2024-01-17T14:21:17Z' + attachments: + - id: 1 + processVersionId: 1 + fileName: some_file.txt + user: test + createDate: '2024-01-17T14:21:17Z' '400': - description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter versionId, Invalid value for: body' + description: 'Invalid value for: header Nu-Impersonate-User-Identity' content: text/plain: schema: @@ -3145,12 +3145,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/comments/{commentId}: - delete: + /api/processes/{scenarioName}/activity/attachments/{attachmentId}: + get: tags: - Activities - summary: Delete process comment service - operationId: deleteApiProcessesScenarionameActivityCommentsCommentid + summary: Download attachment service + operationId: getApiProcessesScenarionameActivityAttachmentsAttachmentid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3164,7 +3164,7 @@ paths: required: true schema: type: string - - name: commentId + - name: attachmentId in: path required: true schema: @@ -3173,9 +3173,25 @@ paths: responses: '200': description: '' + headers: + Content-Disposition: + required: false + schema: + type: + - string + - 'null' + Content-Type: + required: true + schema: + type: string + content: + application/octet-stream: + schema: + type: string + format: binary '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter commentId' + value for: path parameter attachmentId' content: text/plain: schema: @@ -3212,16 +3228,6 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found - '500': - description: '' - content: - text/plain: - schema: - type: string - examples: - Example: - summary: 'Unable to edit comment with id: {commentId}' - value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -3233,16 +3239,15 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation - deprecated: true security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity: - get: + /api/processes/{scenarioName}/activity/comment/{scenarioActivityId}: + put: tags: - Activities - summary: Scenario activity service - operationId: getApiProcessesScenarionameActivity + summary: Edit process comment service + operationId: putApiProcessesScenarionameActivityCommentScenarioactivityid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3256,31 +3261,24 @@ paths: required: true schema: type: string + - name: scenarioActivityId + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + text/plain: + schema: + type: string + required: true responses: '200': description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ProcessActivity' - examples: - Example: - summary: Display scenario activity - value: - comments: - - id: 1 - processVersionId: 1 - content: some comment - user: test - createDate: '2024-01-17T14:21:17Z' - attachments: - - id: 1 - processVersionId: 1 - fileName: some_file.txt - user: test - createDate: '2024-01-17T14:21:17Z' '400': - description: 'Invalid value for: header Nu-Impersonate-User-Identity' + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter scenarioActivityId, Invalid value for: body' content: text/plain: schema: @@ -3317,6 +3315,16 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -3328,16 +3336,14 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation - deprecated: true security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/attachments/{attachmentId}: - get: + delete: tags: - Activities - summary: Download attachment service - operationId: getApiProcessesScenarionameActivityAttachmentsAttachmentid + summary: Delete process comment service + operationId: deleteApiProcessesScenarionameActivityCommentScenarioactivityid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3351,34 +3357,18 @@ paths: required: true schema: type: string - - name: attachmentId + - name: scenarioActivityId in: path required: true schema: - type: integer - format: int64 + type: string + format: uuid responses: '200': description: '' - headers: - Content-Disposition: - required: false - schema: - type: - - string - - 'null' - Content-Type: - required: true - schema: - type: string - content: - application/octet-stream: - schema: - type: string - format: binary '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter attachmentId' + value for: path parameter scenarioActivityId' content: text/plain: schema: @@ -3415,6 +3405,16 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: From 735dfab7449ef2a753cad6819fc296d785e5398d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 10 Sep 2024 15:32:19 +0200 Subject: [PATCH 27/43] qs --- ...DesignerApiAvailableToExposeYamlSpec.scala | 14 +- docs-internal/api/nu-designer-openapi.yaml | 216 +++++++++--------- 2 files changed, 118 insertions(+), 112 deletions(-) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala index 04dcefdfdf7..cd42fe7b2e3 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala @@ -32,7 +32,7 @@ class NuDesignerApiAvailableToExposeYamlSpec extends AnyFunSuite with Matchers { val examplesValidationResult = OpenAPIExamplesValidator.forTapir.validateExamples( specYaml = generatedSpec, excludeResponseValidationForOperationIds = List( - "getApiProcessesScenarionameActivityActivities" // todo NU-1772: responses contains discriminator, it is not properly handled by validator + "getApiProcessesScenarionameActivityActivities" // todo NU-1772: responses contain discriminator, it is not properly handled by validator ) ) val clue = examplesValidationResult @@ -50,9 +50,15 @@ class NuDesignerApiAvailableToExposeYamlSpec extends AnyFunSuite with Matchers { } test("Nu Designer OpenAPI document with all available to expose endpoints has to be up to date") { - val currentNuDesignerOpenApiYamlContent = - (Project.root / "docs-internal" / "api" / "nu-designer-openapi.yaml").contentAsString - NuDesignerApiAvailableToExpose.generateOpenApiYaml should be(currentNuDesignerOpenApiYamlContent) + // todo NU-1772: OpenAPI differs when generated on Scala 2.12 and Scala 2.13 (order of endpoints is different) + // test is for now ignored on Scala 2.12 + if (scala.util.Properties.versionNumberString.startsWith("2.13")) { + val currentNuDesignerOpenApiYamlContent = + (Project.root / "docs-internal" / "api" / "nu-designer-openapi.yaml").contentAsString + NuDesignerApiAvailableToExpose.generateOpenApiYaml should be(currentNuDesignerOpenApiYamlContent) + } else { + info("OpenAPI differs when generated on Scala 2.12 and Scala 2.13. Test is ignored on Scala 2.12") + } } test("API enum compatibility test") { diff --git a/docs-internal/api/nu-designer-openapi.yaml b/docs-internal/api/nu-designer-openapi.yaml index 771bdf5f7db..eeda77e9167 100644 --- a/docs-internal/api/nu-designer-openapi.yaml +++ b/docs-internal/api/nu-designer-openapi.yaml @@ -2870,12 +2870,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/{versionId}/activity/comments: - post: + /api/processes/{scenarioName}/activity/comment/{scenarioActivityId}: + put: tags: - Activities - summary: Add scenario comment service - operationId: postApiProcessesScenarionameVersionidActivityComments + summary: Edit process comment service + operationId: putApiProcessesScenarionameActivityCommentScenarioactivityid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2889,12 +2889,12 @@ paths: required: true schema: type: string - - name: versionId + - name: scenarioActivityId in: path required: true schema: - type: integer - format: int64 + type: string + format: uuid requestBody: content: text/plain: @@ -2906,7 +2906,7 @@ paths: description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter versionId, Invalid value for: body' + value for: path parameter scenarioActivityId, Invalid value for: body' content: text/plain: schema: @@ -2943,6 +2943,16 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -2954,16 +2964,14 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation - deprecated: true security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/comments/{commentId}: delete: tags: - Activities summary: Delete process comment service - operationId: deleteApiProcessesScenarionameActivityCommentsCommentid + operationId: deleteApiProcessesScenarionameActivityCommentScenarioactivityid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -2977,18 +2985,18 @@ paths: required: true schema: type: string - - name: commentId + - name: scenarioActivityId in: path required: true schema: - type: integer - format: int64 + type: string + format: uuid responses: '200': description: '' '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter commentId' + value for: path parameter scenarioActivityId' content: text/plain: schema: @@ -3046,16 +3054,15 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation - deprecated: true security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity: - get: + /api/processes/{scenarioName}/{versionId}/activity/comments: + post: tags: - Activities - summary: Scenario activity service - operationId: getApiProcessesScenarionameActivity + summary: Add scenario comment service + operationId: postApiProcessesScenarionameVersionidActivityComments parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3069,31 +3076,24 @@ paths: required: true schema: type: string + - name: versionId + in: path + required: true + schema: + type: integer + format: int64 + requestBody: + content: + text/plain: + schema: + type: string + required: true responses: '200': description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/ProcessActivity' - examples: - Example: - summary: Display scenario activity - value: - comments: - - id: 1 - processVersionId: 1 - content: some comment - user: test - createDate: '2024-01-17T14:21:17Z' - attachments: - - id: 1 - processVersionId: 1 - fileName: some_file.txt - user: test - createDate: '2024-01-17T14:21:17Z' '400': - description: 'Invalid value for: header Nu-Impersonate-User-Identity' + description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid + value for: path parameter versionId, Invalid value for: body' content: text/plain: schema: @@ -3145,12 +3145,12 @@ paths: security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/attachments/{attachmentId}: - get: + /api/processes/{scenarioName}/activity/comments/{commentId}: + delete: tags: - Activities - summary: Download attachment service - operationId: getApiProcessesScenarionameActivityAttachmentsAttachmentid + summary: Delete process comment service + operationId: deleteApiProcessesScenarionameActivityCommentsCommentid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3164,7 +3164,7 @@ paths: required: true schema: type: string - - name: attachmentId + - name: commentId in: path required: true schema: @@ -3173,25 +3173,9 @@ paths: responses: '200': description: '' - headers: - Content-Disposition: - required: false - schema: - type: - - string - - 'null' - Content-Type: - required: true - schema: - type: string - content: - application/octet-stream: - schema: - type: string - format: binary '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter attachmentId' + value for: path parameter commentId' content: text/plain: schema: @@ -3228,6 +3212,16 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found + '500': + description: '' + content: + text/plain: + schema: + type: string + examples: + Example: + summary: 'Unable to edit comment with id: {commentId}' + value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -3239,15 +3233,16 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation + deprecated: true security: - {} - httpAuth: [] - /api/processes/{scenarioName}/activity/comment/{scenarioActivityId}: - put: + /api/processes/{scenarioName}/activity: + get: tags: - Activities - summary: Edit process comment service - operationId: putApiProcessesScenarionameActivityCommentScenarioactivityid + summary: Scenario activity service + operationId: getApiProcessesScenarionameActivity parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3261,24 +3256,31 @@ paths: required: true schema: type: string - - name: scenarioActivityId - in: path - required: true - schema: - type: string - format: uuid - requestBody: - content: - text/plain: - schema: - type: string - required: true responses: '200': description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessActivity' + examples: + Example: + summary: Display scenario activity + value: + comments: + - id: 1 + processVersionId: 1 + content: some comment + user: test + createDate: '2024-01-17T14:21:17Z' + attachments: + - id: 1 + processVersionId: 1 + fileName: some_file.txt + user: test + createDate: '2024-01-17T14:21:17Z' '400': - description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter scenarioActivityId, Invalid value for: body' + description: 'Invalid value for: header Nu-Impersonate-User-Identity' content: text/plain: schema: @@ -3315,16 +3317,6 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found - '500': - description: '' - content: - text/plain: - schema: - type: string - examples: - Example: - summary: 'Unable to edit comment with id: {commentId}' - value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -3336,14 +3328,16 @@ paths: summary: Cannot authenticate impersonated user as impersonation is not supported by the authentication mechanism value: Provided authentication method does not support impersonation + deprecated: true security: - {} - httpAuth: [] - delete: + /api/processes/{scenarioName}/activity/attachments/{attachmentId}: + get: tags: - Activities - summary: Delete process comment service - operationId: deleteApiProcessesScenarionameActivityCommentScenarioactivityid + summary: Download attachment service + operationId: getApiProcessesScenarionameActivityAttachmentsAttachmentid parameters: - name: Nu-Impersonate-User-Identity in: header @@ -3357,18 +3351,34 @@ paths: required: true schema: type: string - - name: scenarioActivityId + - name: attachmentId in: path required: true schema: - type: string - format: uuid + type: integer + format: int64 responses: '200': description: '' + headers: + Content-Disposition: + required: false + schema: + type: + - string + - 'null' + Content-Type: + required: true + schema: + type: string + content: + application/octet-stream: + schema: + type: string + format: binary '400': description: 'Invalid value for: header Nu-Impersonate-User-Identity, Invalid - value for: path parameter scenarioActivityId' + value for: path parameter attachmentId' content: text/plain: schema: @@ -3405,16 +3415,6 @@ paths: Example: summary: No scenario {scenarioName} found value: No scenario 'example scenario' found - '500': - description: '' - content: - text/plain: - schema: - type: string - examples: - Example: - summary: 'Unable to edit comment with id: {commentId}' - value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: From 77a4a78d96837c98c9406927d9da450b5205b0eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 16 Sep 2024 11:13:55 +0200 Subject: [PATCH 28/43] review fixes --- .../description/scenarioActivity/Dtos.scala | 52 ++++++++- .../scenarioActivity/Examples.scala | 16 +-- ...DesignerApiAvailableToExposeYamlSpec.scala | 5 +- docs-internal/api/nu-designer-openapi.yaml | 102 +++++++++++++++--- 4 files changed, 148 insertions(+), 27 deletions(-) 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 bbae3da1bb6..76e29b20b57 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 @@ -231,16 +231,64 @@ object Dtos { } @derive(encoder, decoder, schema) - final case class ScenarioActivityComment(comment: Option[String], lastModifiedBy: String, lastModifiedAt: Instant) + final case class ScenarioActivityComment( + status: ScenarioActivityCommentStatus, + lastModifiedBy: String, + lastModifiedAt: Instant + ) + + sealed trait ScenarioActivityCommentStatus + + object ScenarioActivityCommentStatus { + + implicit def scenarioActivityAttachmentStatusCodec: circe.Codec[ScenarioActivityCommentStatus] = { + implicit val configuration: extras.Configuration = + extras.Configuration.default.withDiscriminator("type").withScreamingSnakeCaseConstructorNames + deriveConfiguredCodec + } + + implicit def scenarioActivityAttachmentStatusSchema: Schema[ScenarioActivityCommentStatus] = { + implicit val configuration: Configuration = + Configuration.default.withDiscriminator("type").withScreamingSnakeCaseDiscriminatorValues + Schema.derived[ScenarioActivityCommentStatus] + } + + final case class Available(comment: String) extends ScenarioActivityCommentStatus + + case object Deleted extends ScenarioActivityCommentStatus + + } @derive(encoder, decoder, schema) final case class ScenarioActivityAttachment( - id: Option[Long], + status: ScenarioActivityAttachmentStatus, filename: String, lastModifiedBy: String, lastModifiedAt: Instant ) + sealed trait ScenarioActivityAttachmentStatus + + object ScenarioActivityAttachmentStatus { + + implicit def scenarioActivityAttachmentStatusCodec: circe.Codec[ScenarioActivityAttachmentStatus] = { + implicit val configuration: extras.Configuration = + extras.Configuration.default.withDiscriminator("type").withScreamingSnakeCaseConstructorNames + deriveConfiguredCodec + } + + implicit def scenarioActivityAttachmentStatusSchema: Schema[ScenarioActivityAttachmentStatus] = { + implicit val configuration: Configuration = + Configuration.default.withDiscriminator("type").withScreamingSnakeCaseDiscriminatorValues + Schema.derived[ScenarioActivityAttachmentStatus] + } + + final case class Available(id: Long) extends ScenarioActivityAttachmentStatus + + case object Deleted extends ScenarioActivityAttachmentStatus + + } + @derive(encoder, decoder, schema) final case class ScenarioActivities(activities: List[ScenarioActivity]) 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 1050cd95c99..8088593b920 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 @@ -62,7 +62,7 @@ object Examples { date = Instant.parse("2024-01-17T14:21:17Z"), scenarioVersion = Some(1), comment = ScenarioActivityComment( - comment = Some("Deployment of scenario - task JIRA-1234"), + status = ScenarioActivityCommentStatus.Available("Deployment of scenario - task JIRA-1234"), lastModifiedBy = "some user", lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") ) @@ -73,7 +73,7 @@ object Examples { date = Instant.parse("2024-01-17T14:21:17Z"), scenarioVersion = Some(1), comment = ScenarioActivityComment( - comment = Some("Canceled because marketing campaign ended"), + status = ScenarioActivityCommentStatus.Available("Canceled because marketing campaign ended"), lastModifiedBy = "some user", lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") ) @@ -84,7 +84,7 @@ object Examples { date = Instant.parse("2024-01-17T14:21:17Z"), scenarioVersion = Some(1), comment = ScenarioActivityComment( - comment = Some("Added new processing step"), + status = ScenarioActivityCommentStatus.Available("Added new processing step"), lastModifiedBy = "some user", lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") ) @@ -103,7 +103,7 @@ object Examples { date = Instant.parse("2024-01-17T14:21:17Z"), scenarioVersion = Some(1), comment = ScenarioActivityComment( - comment = Some("Added new processing step"), + status = ScenarioActivityCommentStatus.Available("Added new processing step"), lastModifiedBy = "some user", lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") ) @@ -114,7 +114,7 @@ object Examples { date = Instant.parse("2024-01-17T14:21:17Z"), scenarioVersion = Some(1), comment = ScenarioActivityComment( - comment = None, + status = ScenarioActivityCommentStatus.Deleted, lastModifiedBy = "John Doe", lastModifiedAt = Instant.parse("2024-01-18T14:21:17Z") ) @@ -125,7 +125,7 @@ object Examples { date = Instant.parse("2024-01-17T14:21:17Z"), scenarioVersion = Some(1), attachment = ScenarioActivityAttachment( - id = Some(10000001), + status = ScenarioActivityAttachmentStatus.Available(10000001), filename = "attachment01.png", lastModifiedBy = "some user", lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") @@ -137,7 +137,7 @@ object Examples { date = Instant.parse("2024-01-17T14:21:17Z"), scenarioVersion = Some(1), attachment = ScenarioActivityAttachment( - id = None, + status = ScenarioActivityAttachmentStatus.Deleted, filename = "attachment01.png", lastModifiedBy = "John Doe", lastModifiedAt = Instant.parse("2024-01-18T14:21:17Z") @@ -165,7 +165,7 @@ object Examples { date = Instant.parse("2024-01-17T14:21:17Z"), scenarioVersion = Some(1), comment = ScenarioActivityComment( - comment = Some("Added new processing step"), + status = ScenarioActivityCommentStatus.Available("Added new processing step"), lastModifiedBy = "some user", lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") ), diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala index cd42fe7b2e3..bb9cd3cc7ff 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NuDesignerApiAvailableToExposeYamlSpec.scala @@ -50,8 +50,9 @@ class NuDesignerApiAvailableToExposeYamlSpec extends AnyFunSuite with Matchers { } test("Nu Designer OpenAPI document with all available to expose endpoints has to be up to date") { - // todo NU-1772: OpenAPI differs when generated on Scala 2.12 and Scala 2.13 (order of endpoints is different) - // test is for now ignored on Scala 2.12 + // OpenAPI differs when generated on Scala 2.12 and Scala 2.13 (order of endpoints is different). + // - test is ignored on Scala 2.12, + // - it is probably not necessary to fix it, because we plan to remove Scala 2.12 support anyway if (scala.util.Properties.versionNumberString.startsWith("2.13")) { val currentNuDesignerOpenApiYamlContent = (Project.root / "docs-internal" / "api" / "nu-designer-openapi.yaml").contentAsString diff --git a/docs-internal/api/nu-designer-openapi.yaml b/docs-internal/api/nu-designer-openapi.yaml index eeda77e9167..6821cd3bf28 100644 --- a/docs-internal/api/nu-designer-openapi.yaml +++ b/docs-internal/api/nu-designer-openapi.yaml @@ -3480,7 +3480,9 @@ paths: date: '2024-01-17T14:21:17Z' scenarioVersion: 1 comment: - comment: Deployment of scenario - task JIRA-1234 + status: + comment: Deployment of scenario - task JIRA-1234 + type: AVAILABLE lastModifiedBy: some user lastModifiedAt: '2024-01-17T14:21:17Z' type: SCENARIO_DEPLOYED @@ -3489,7 +3491,9 @@ paths: date: '2024-01-17T14:21:17Z' scenarioVersion: 1 comment: - comment: Canceled because marketing campaign ended + status: + comment: Canceled because marketing campaign ended + type: AVAILABLE lastModifiedBy: some user lastModifiedAt: '2024-01-17T14:21:17Z' type: SCENARIO_CANCELED @@ -3498,7 +3502,9 @@ paths: date: '2024-01-17T14:21:17Z' scenarioVersion: 1 comment: - comment: Added new processing step + status: + comment: Added new processing step + type: AVAILABLE lastModifiedBy: some user lastModifiedAt: '2024-01-17T14:21:17Z' type: SCENARIO_MODIFIED @@ -3514,7 +3520,9 @@ paths: date: '2024-01-17T14:21:17Z' scenarioVersion: 1 comment: - comment: Added new processing step + status: + comment: Added new processing step + type: AVAILABLE lastModifiedBy: some user lastModifiedAt: '2024-01-17T14:21:17Z' type: COMMENT_ADDED @@ -3523,6 +3531,8 @@ paths: date: '2024-01-17T14:21:17Z' scenarioVersion: 1 comment: + status: + type: DELETED lastModifiedBy: John Doe lastModifiedAt: '2024-01-18T14:21:17Z' type: COMMENT_ADDED @@ -3531,7 +3541,9 @@ paths: date: '2024-01-17T14:21:17Z' scenarioVersion: 1 attachment: - id: 10000001 + status: + id: 10000001 + type: AVAILABLE filename: attachment01.png lastModifiedBy: some user lastModifiedAt: '2024-01-17T14:21:17Z' @@ -3541,6 +3553,8 @@ paths: date: '2024-01-17T14:21:17Z' scenarioVersion: 1 attachment: + status: + type: DELETED filename: attachment01.png lastModifiedBy: John Doe lastModifiedAt: '2024-01-18T14:21:17Z' @@ -3564,7 +3578,9 @@ paths: date: '2024-01-17T14:21:17Z' scenarioVersion: 1 comment: - comment: Added new processing step + status: + comment: Added new processing step + type: AVAILABLE lastModifiedBy: some user lastModifiedAt: '2024-01-17T14:21:17Z' destinationEnvironment: preprod @@ -3939,6 +3955,29 @@ components: - 'null' type: type: string + Available: + title: Available + type: object + required: + - id + - type + properties: + id: + type: integer + format: int64 + type: + type: string + Available1: + title: Available + type: object + required: + - comment + - type + properties: + comment: + type: string + type: + type: string BoolParameterEditor: title: BoolParameterEditor type: object @@ -4282,6 +4321,22 @@ components: DateTimeParameterEditor: title: DateTimeParameterEditor type: object + Deleted: + title: Deleted + type: object + required: + - type + properties: + type: + type: string + Deleted1: + title: Deleted + type: object + required: + - type + properties: + type: + type: string Dict: title: Dict type: object @@ -5741,15 +5796,13 @@ components: title: ScenarioActivityAttachment type: object required: + - status - filename - lastModifiedBy - lastModifiedAt properties: - id: - type: - - integer - - 'null' - format: int64 + status: + $ref: '#/components/schemas/ScenarioActivityAttachmentStatus' filename: type: string lastModifiedBy: @@ -5757,22 +5810,41 @@ components: lastModifiedAt: type: string format: date-time + ScenarioActivityAttachmentStatus: + title: ScenarioActivityAttachmentStatus + oneOf: + - $ref: '#/components/schemas/Available' + - $ref: '#/components/schemas/Deleted' + discriminator: + propertyName: type + mapping: + AVAILABLE: '#/components/schemas/Available' + DELETED: '#/components/schemas/Deleted' ScenarioActivityComment: title: ScenarioActivityComment type: object required: + - status - lastModifiedBy - lastModifiedAt properties: - comment: - type: - - string - - 'null' + status: + $ref: '#/components/schemas/ScenarioActivityCommentStatus' lastModifiedBy: type: string lastModifiedAt: type: string format: date-time + ScenarioActivityCommentStatus: + title: ScenarioActivityCommentStatus + oneOf: + - $ref: '#/components/schemas/Available1' + - $ref: '#/components/schemas/Deleted1' + discriminator: + propertyName: type + mapping: + AVAILABLE: '#/components/schemas/Available1' + DELETED: '#/components/schemas/Deleted1' ScenarioActivityMetadata: title: ScenarioActivityMetadata type: object From cf05ecbab65e09d430379434564aa94013d64311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 16 Sep 2024 12:25:12 +0200 Subject: [PATCH 29/43] merge fixes --- .../ui/api/ScenarioActivityApiHttpService.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 e2f5cf38d08..8ecdb112550 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 @@ -227,13 +227,13 @@ class ScenarioActivityApiHttpService( scenarioComment match { case ScenarioComment.Available(comment, lastModifiedByUserName) => Dtos.ScenarioActivityComment( - comment = Some(comment), + status = Dtos.ScenarioActivityCommentStatus.Available(comment), lastModifiedBy = lastModifiedByUserName.value, lastModifiedAt = Instant.now(), ) case ScenarioComment.Deleted(deletedByUserName) => Dtos.ScenarioActivityComment( - comment = None, + status = Dtos.ScenarioActivityCommentStatus.Deleted, lastModifiedBy = deletedByUserName.value, lastModifiedAt = Instant.now(), ) @@ -244,14 +244,14 @@ class ScenarioActivityApiHttpService( attachment match { case ScenarioAttachment.Available(attachmentId, attachmentFilename, lastModifiedByUserName) => Dtos.ScenarioActivityAttachment( - id = Some(attachmentId.value), + status = Dtos.ScenarioActivityAttachmentStatus.Available(attachmentId.value), filename = attachmentFilename.value, lastModifiedBy = lastModifiedByUserName.value, lastModifiedAt = Instant.now() ) case ScenarioAttachment.Deleted(attachmentFilename, deletedByUserName) => Dtos.ScenarioActivityAttachment( - id = None, + status = Dtos.ScenarioActivityAttachmentStatus.Deleted, filename = attachmentFilename.value, lastModifiedBy = deletedByUserName.value, lastModifiedAt = Instant.now() From 8c90c6846b84f5bdd2f63fafde9dd9075b286a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 16 Sep 2024 23:30:27 +0200 Subject: [PATCH 30/43] review fixes --- ...__CreateScenarioActivitiesDefinition.scala | 45 ++++- ...mmentsToScenarioActivitiesDefinition.scala | 115 +++++------ .../api/ScenarioActivityApiHttpService.scala | 17 +- .../ScenarioActivityEntityFactory.scala | 12 +- .../ui/initialization/Initialization.scala | 5 +- .../notifications/NotificationService.scala | 18 +- .../ui/process/ProcessService.scala | 10 +- ...cessingTypeDeployedScenariosProvider.scala | 2 +- .../deployment/DeploymentService.scala | 2 +- .../process/newactivity/ActivityService.scala | 12 +- .../DBFetchingProcessRepository.scala | 6 +- .../repository/ProcessRepository.scala | 7 +- ...y.scala => ScenarioActionRepository.scala} | 7 +- .../DbScenarioActivityRepository.scala | 180 +++++++++++------- .../server/AkkaHttpBasedRouteProvider.scala | 14 +- ...tionsAndCommentsToScenarioActivities.scala | 103 ++++++---- .../nussknacker/test/base/db/DbTesting.scala | 1 + .../test/base/it/NuResourcesTest.scala | 12 +- ...sControlCheckingConfigScenarioHelper.scala | 4 +- .../it/WithBatchConfigScenarioHelper.scala | 4 +- ...UsedMoreThanOnceConfigScenarioHelper.scala | 4 +- .../WithSimplifiedConfigScenarioHelper.scala | 4 +- .../test/utils/domain/ScenarioHelper.scala | 11 +- .../test/utils/domain/TestFactory.scala | 13 +- ...tApiHttpServiceDeploymentCommentSpec.scala | 2 - .../DefaultComponentServiceSpec.scala | 2 +- .../InitializationOnDbItSpec.scala | 16 +- .../NotificationServiceTest.scala | 6 +- .../ui/process/DBProcessServiceSpec.scala | 2 +- .../deployment/DeploymentServiceSpec.scala | 6 +- .../DeploymentRepositorySpec.scala | 4 +- .../newdeployment/DeploymentServiceTest.scala | 4 +- .../DBFetchingProcessRepositorySpec.scala | 8 +- .../engine/api/deployment/ProcessAction.scala | 3 + .../api/deployment/ProcessActivity.scala | 6 +- .../src/main/resources/logback-test.xml | 2 - 36 files changed, 404 insertions(+), 265 deletions(-) rename designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/{ProcessActionRepository.scala => ScenarioActionRepository.scala} (99%) diff --git a/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala index 0f7f5ae2e78..0ccdf70460a 100644 --- a/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala +++ b/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala @@ -2,7 +2,6 @@ package db.migration import com.typesafe.scalalogging.LazyLogging import db.migration.V1_055__CreateScenarioActivitiesDefinition.ScenarioActivitiesDefinitions -import db.migration.V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.logger import pl.touk.nussknacker.ui.db.migration.SlickMigration import slick.jdbc.JdbcProfile import slick.sql.SqlProfile.ColumnOption.NotNull @@ -52,6 +51,8 @@ object V1_055__CreateScenarioActivitiesDefinition { 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") @@ -70,6 +71,46 @@ object V1_055__CreateScenarioActivitiesDefinition { def additionalProperties: Rep[String] = column[String]("additional_properties", NotNull) + def tuple: ( + Rep[String], + Rep[Long], + Rep[UUID], + Rep[Option[String]], + Rep[String], + Rep[Option[String]], + Rep[Option[String]], + Rep[Option[String]], + Rep[Option[Timestamp]], + Rep[Timestamp], + Rep[Option[Long]], + Rep[Option[String]], + Rep[Option[Long]], + Rep[Option[Timestamp]], + Rep[Option[String]], + Rep[Option[String]], + Rep[Option[String]], + Rep[String] + ) = ( + activityType, + scenarioId, + activityId, + userId, + userName, + impersonatedByUserId, + impersonatedByUserName, + lastModifiedByUserName, + lastModifiedAt, + createdAt, + scenarioVersion, + comment, + attachmentId, + performedAt, + state, + errorMessage, + buildInfo, + additionalProperties, + ) + override def * = ( id, @@ -81,6 +122,7 @@ object V1_055__CreateScenarioActivitiesDefinition { impersonatedByUserId, impersonatedByUserName, lastModifiedByUserName, + lastModifiedAt, createdAt, scenarioVersion, comment, @@ -108,6 +150,7 @@ object V1_055__CreateScenarioActivitiesDefinition { impersonatedByUserId: Option[String], impersonatedByUserName: Option[String], lastModifiedByUserName: Option[String], + lastModifiedAt: Option[Timestamp], createdAt: Timestamp, scenarioVersion: Option[Long], comment: Option[String], diff --git a/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala index 8511a371567..4e03b764219 100644 --- a/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala +++ b/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala @@ -1,15 +1,11 @@ package db.migration import com.typesafe.scalalogging.LazyLogging -import db.migration.V1_055__CreateScenarioActivitiesDefinition.{ - ScenarioActivitiesDefinitions, - ScenarioActivityEntityData -} +import db.migration.V1_055__CreateScenarioActivitiesDefinition.ScenarioActivitiesDefinitions import db.migration.V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.Migration -import io.circe.syntax.EncoderOps import pl.touk.nussknacker.engine.api.deployment.ScenarioActionName import pl.touk.nussknacker.engine.management.periodic.InstantBatchCustomAction -import pl.touk.nussknacker.ui.db.entity.ScenarioActivityType +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} @@ -17,7 +13,6 @@ import slick.sql.SqlProfile.ColumnOption.NotNull import java.sql.Timestamp import java.util.UUID -import scala.concurrent.ExecutionContext.Implicits.global trait V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends SlickMigration with LazyLogging { @@ -31,7 +26,7 @@ trait V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends Sl object V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends LazyLogging { - class Migration(val profile: JdbcProfile) { + class Migration(val profile: JdbcProfile) extends ScenarioActivityEntityFactory { import profile.api._ @@ -39,64 +34,58 @@ object V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends L private val processActionsDefinitions = new ProcessActionsDefinitions(profile) private val commentsDefinitions = new CommentsDefinitions(profile) - def migrateActions: DBIOAction[(List[ScenarioActivityEntityData], Int), NoStream, Effect.All] = { - logger.info("Executing migration V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition") - for { - actionsWithComments <- - processActionsDefinitions.table - .joinLeft(commentsDefinitions.table) - .on(_.commentId === _.id) - .result - _ = logger.info(s"There are ${actionsWithComments.length} process actions to migrate") - activities = - actionsWithComments.map { case (processAction, maybeComment) => - ScenarioActivityEntityData( - id = -1L, - activityType = activityTypeStr(processAction.actionName), - scenarioId = processAction.processId, - activityId = processAction.id, - userId = None, - userName = processAction.user, - impersonatedByUserId = processAction.impersonatedByIdentity, - impersonatedByUserName = processAction.impersonatedByUsername, - lastModifiedByUserName = Some(processAction.user), - createdAt = processAction.createdAt, - scenarioVersion = processAction.processVersionId, - comment = maybeComment.map(_.content), - attachmentId = None, - finishedAt = processAction.performedAt, - state = Some(processAction.state), - errorMessage = processAction.failureMessage, - buildInfo = None, - additionalProperties = Map.empty[String, String].asJson.noSpaces + 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), // comment + None: Option[Long], // attachmentId + processAction.performedAt, // finishedAt + processAction.state.?, // state + processAction.failureMessage, // errorMessage + None: Option[String], // buildInfo - always absent in old actions + "{}" // additionalProperties - always empty in old actions ) - }.toList - _ = logger.info(s"Created ${activities.length} scenario activities based on preexisting actions") - count <- DBIO.sequence(activities.map(scenarioActivitiesDefinitions.scenarioActivitiesTable += _)).map(_.sum) - _ = logger.info(s"Inserted $count scenario activities to the db") - } yield (activities, count) + } + + // Slick generates single "insert from select" query and operation is performed solely on db + scenarioActivitiesDefinitions.scenarioActivitiesTable.map(_.tuple).forceInsertQuery(insertQuery) } - private def activityTypeStr(actionName: String) = { - val activityType = ScenarioActionName(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) - } - activityType.entryName + def activityType(actionNameRep: Rep[String]): Rep[String] = { + val customActionPrefix = s"CUSTOM_ACTION_[" + val customActionSuffix = "]" + Case + .If(actionNameRep === ScenarioActionName.Deploy.value) + .Then(ScenarioActivityType.ScenarioDeployed.entryName) + .If(actionNameRep === ScenarioActionName.Cancel.value) + .Then(ScenarioActivityType.ScenarioCanceled.entryName) + .If(actionNameRep === ScenarioActionName.Archive.value) + .Then(ScenarioActivityType.ScenarioArchived.entryName) + .If(actionNameRep === ScenarioActionName.UnArchive.value) + .Then(ScenarioActivityType.ScenarioUnarchived.entryName) + .If(actionNameRep === ScenarioActionName.Pause.value) + .Then(ScenarioActivityType.ScenarioPaused.entryName) + .If(actionNameRep === ScenarioActionName.Rename.value) + .Then(ScenarioActivityType.ScenarioNameChanged.entryName) + .If(actionNameRep === InstantBatchCustomAction.name.value) + .Then(ScenarioActivityType.PerformedSingleExecution.entryName) + .Else(actionNameRep.reverseString.++(customActionPrefix.reverse).reverseString.++(customActionSuffix)) } } 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 8ecdb112550..667b4338d19 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 @@ -28,7 +28,6 @@ import sttp.model.MediaType import java.io.ByteArrayInputStream import java.net.URLConnection -import java.time.Instant import scala.concurrent.{ExecutionContext, Future} class ScenarioActivityApiHttpService( @@ -225,36 +224,36 @@ class ScenarioActivityApiHttpService( private def toDto(scenarioComment: ScenarioComment): Dtos.ScenarioActivityComment = { scenarioComment match { - case ScenarioComment.Available(comment, lastModifiedByUserName) => + case ScenarioComment.Available(comment, lastModifiedByUserName, lastModifiedAt) => Dtos.ScenarioActivityComment( status = Dtos.ScenarioActivityCommentStatus.Available(comment), lastModifiedBy = lastModifiedByUserName.value, - lastModifiedAt = Instant.now(), + lastModifiedAt = lastModifiedAt, ) - case ScenarioComment.Deleted(deletedByUserName) => + case ScenarioComment.Deleted(deletedByUserName, deletedAt) => Dtos.ScenarioActivityComment( status = Dtos.ScenarioActivityCommentStatus.Deleted, lastModifiedBy = deletedByUserName.value, - lastModifiedAt = Instant.now(), + lastModifiedAt = deletedAt, ) } } private def toDto(attachment: ScenarioAttachment): Dtos.ScenarioActivityAttachment = { attachment match { - case ScenarioAttachment.Available(attachmentId, attachmentFilename, lastModifiedByUserName) => + case ScenarioAttachment.Available(attachmentId, attachmentFilename, lastModifiedByUserName, lastModifiedAt) => Dtos.ScenarioActivityAttachment( status = Dtos.ScenarioActivityAttachmentStatus.Available(attachmentId.value), filename = attachmentFilename.value, lastModifiedBy = lastModifiedByUserName.value, - lastModifiedAt = Instant.now() + lastModifiedAt = lastModifiedAt, ) - case ScenarioAttachment.Deleted(attachmentFilename, deletedByUserName) => + case ScenarioAttachment.Deleted(attachmentFilename, deletedByUserName, deletedAt) => Dtos.ScenarioActivityAttachment( status = Dtos.ScenarioActivityAttachmentStatus.Deleted, filename = attachmentFilename.value, lastModifiedBy = deletedByUserName.value, - lastModifiedAt = Instant.now() + lastModifiedAt = deletedAt, ) } } 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 index 94450388a54..fd4056d2459 100644 --- 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 @@ -43,6 +43,8 @@ trait ScenarioActivityEntityFactory extends BaseEntityFactory { 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") @@ -72,6 +74,7 @@ trait ScenarioActivityEntityFactory extends BaseEntityFactory { impersonatedByUserId, impersonatedByUserName, lastModifiedByUserName, + lastModifiedAt, createdAt, scenarioVersion, comment, @@ -162,7 +165,13 @@ object ScenarioActivityType extends Enum[ScenarioActivityType] { } -final case class AdditionalProperties(properties: Map[String, String]) +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) @@ -178,6 +187,7 @@ final case class ScenarioActivityEntityData( impersonatedByUserId: Option[String], impersonatedByUserName: Option[String], lastModifiedByUserName: Option[String], + lastModifiedAt: Option[Timestamp], createdAt: Timestamp, scenarioVersion: Option[ScenarioVersion], comment: Option[String], 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 de5381d8de8..67ba44abb4f 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 @@ -17,6 +17,7 @@ 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,11 +28,13 @@ object Initialization { def init( migrations: ProcessingTypeDataProvider[ProcessMigrations, _], db: DbRef, + clock: Clock, fetchingRepository: DBFetchingProcessRepository[DB], scenarioActivityRepository: ScenarioActivityRepository, environment: String, )(implicit ec: ExecutionContext): Unit = { - val processRepository = new DBProcessRepository(db, scenarioActivityRepository, migrations.mapValues(_.version)) + val processRepository = + new DBProcessRepository(db, clock, scenarioActivityRepository, 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 e0d6c2078e0..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,7 +1,7 @@ package pl.touk.nussknacker.ui.notifications import pl.touk.nussknacker.engine.api.deployment.{ProcessActionState, ScenarioActionName} -import pl.touk.nussknacker.ui.process.repository.{DBIOActionRunner, ProcessActionRepository} +import pl.touk.nussknacker.ui.process.repository.{DBIOActionRunner, ScenarioActionRepository} import pl.touk.nussknacker.ui.security.api.LoggedUser import java.time.Clock @@ -17,7 +17,7 @@ trait NotificationService { } class NotificationServiceImpl( - processActionRepository: ProcessActionRepository, + scenarioActionRepository: ScenarioActionRepository, dbioRunner: DBIOActionRunner, config: NotificationConfig, clock: Clock = Clock.systemUTC() @@ -28,26 +28,28 @@ class NotificationServiceImpl( val limit = now.minusMillis(config.duration.toMillis) dbioRunner .run( - processActionRepository.getUserActionsAfter( + scenarioActionRepository.getUserActionsAfter( user, Set(ScenarioActionName.Deploy, ScenarioActionName.Cancel), ProcessActionState.FinishedStates + ProcessActionState.Failed, limit ) ) - .map(_.map { case (action, processName) => + .map(_.map { case (action, scenarioName) => action.state match { case ProcessActionState.Finished => Notification - .actionFinishedNotification(action.id.toString, action.actionName, processName) + .actionFinishedNotification(action.id.toString, action.actionName, scenarioName) case ProcessActionState.Failed => Notification - .actionFailedNotification(action.id.toString, action.actionName, processName, action.failureMessage) + .actionFailedNotification(action.id.toString, action.actionName, scenarioName, action.failureMessage) case ProcessActionState.ExecutionFinished => Notification - .actionExecutionFinishedNotification(action.id.toString, action.actionName, processName) + .actionExecutionFinishedNotification(action.id.toString, action.actionName, scenarioName) case ProcessActionState.InProgress => - throw new IllegalStateException(s"Unexpected action returned by query: $action, for scenario: $processName") + 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 3fb5728a0f3..031f9f2ae86 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 @@ -158,7 +158,7 @@ class DBProcessService( processResolverByProcessingType: ProcessingTypeDataProvider[UIProcessResolver, _], dbioRunner: DBIOActionRunner, fetchingProcessRepository: FetchingProcessRepository[Future], - processActionRepository: ProcessActionRepository, + scenarioActionRepository: ScenarioActionRepository, processRepository: ProcessRepository[DB] )(implicit ec: ExecutionContext) extends ProcessService @@ -334,7 +334,7 @@ class DBProcessService( .runInTransaction( DBIOAction.seq( processRepository.archive(processId = process.idWithNameUnsafe, isArchived = false), - processActionRepository + scenarioActionRepository .markProcessAsUnArchived(processId = process.processIdUnsafe, process.processVersionId) ) ) @@ -466,7 +466,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) ) ) @@ -479,7 +479,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/deployment/DefaultProcessingTypeDeployedScenariosProvider.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/deployment/DefaultProcessingTypeDeployedScenariosProvider.scala index 61f37ff8fc1..6fe9104f4aa 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,7 +85,7 @@ object DefaultProcessingTypeDeployedScenariosProvider { val dumbModelInfoProvier = ProcessingTypeDataProvider.withEmptyCombinedData( Map(processingType -> ValueWithRestriction.anyUser(Map.empty[String, String])) ) - val actionRepository = new DbProcessActionRepository(dbRef, dumbModelInfoProvier) + val actionRepository = new DbScenarioActionRepository(dbRef, dumbModelInfoProvier) val processRepository = DBFetchingProcessRepository.create(dbRef, actionRepository) val futureProcessRepository = DBFetchingProcessRepository.createFutureRepository(dbRef, actionRepository) new DefaultProcessingTypeDeployedScenariosProvider( 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 ddd4195905e..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, _], 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 edddde466f3..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 @@ -12,7 +12,7 @@ import pl.touk.nussknacker.ui.process.repository.activities.ScenarioActivityRepo import pl.touk.nussknacker.ui.process.repository.{DBIOActionRunner, DeploymentComment} import pl.touk.nussknacker.ui.security.api.LoggedUser -import java.time.Instant +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. @@ -21,7 +21,8 @@ class ActivityService( deploymentCommentSettings: Option[DeploymentCommentSettings], scenarioActivityRepository: ScenarioActivityRepository, deploymentService: DeploymentService, - dbioRunner: DBIOActionRunner + dbioRunner: DBIOActionRunner, + clock: Clock, )(implicit ec: ExecutionContext) { def processCommand[Command, ErrorType](command: Command, comment: Option[Comment])( @@ -62,6 +63,7 @@ class ActivityService( scenarioGraphVersionId: VersionId, loggedUser: LoggedUser ): EitherT[Future, ActivityError[ErrorType], Unit] = { + val now = clock.instant() EitherT.right[ActivityError[ErrorType]]( dbioRunner .run( @@ -75,11 +77,11 @@ class ActivityService( impersonatedByUserId = loggedUser.impersonatingUserId.map(UserId.apply), impersonatedByUserName = loggedUser.impersonatingUserName.map(UserName.apply) ), - date = Instant.now(), + date = now, scenarioVersion = Some(ScenarioVersion(scenarioGraphVersionId.value)), comment = commentOpt match { - case Some(comment) => ScenarioComment.Available(comment.value, UserName(loggedUser.username)) - case None => ScenarioComment.Deleted(UserName(loggedUser.username)) + case Some(comment) => ScenarioComment.Available(comment.value, UserName(loggedUser.username), now) + case None => ScenarioComment.Deleted(UserName(loggedUser.username), now) }, ) )(loggedUser) 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 d21bcbacbc3..15e29f3c517 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 @@ -19,10 +19,10 @@ import scala.language.higherKinds object DBFetchingProcessRepository { - def create(dbRef: DbRef, actionRepository: ProcessActionRepository)(implicit ec: ExecutionContext) = + def create(dbRef: DbRef, actionRepository: ScenarioActionRepository)(implicit ec: ExecutionContext) = new DBFetchingProcessRepository[DB](dbRef, actionRepository) with DbioRepository - def createFutureRepository(dbRef: DbRef, actionRepository: ProcessActionRepository)( + def createFutureRepository(dbRef: DbRef, actionRepository: ScenarioActionRepository)( implicit ec: ExecutionContext ) = new DBFetchingProcessRepository[Future](dbRef, actionRepository) with BasicRepository @@ -34,7 +34,7 @@ object DBFetchingProcessRepository { // to the resource on the services side abstract class DBFetchingProcessRepository[F[_]: Monad]( protected val dbRef: DbRef, - actionRepository: ProcessActionRepository + actionRepository: ScenarioActionRepository )(protected implicit val ec: ExecutionContext) extends FetchingProcessRepository[F] with LazyLogging { 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 4dc919a0a21..f928a4825da 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 @@ -25,7 +25,7 @@ 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 @@ -45,10 +45,11 @@ object ProcessRepository { def create( dbRef: DbRef, + clock: Clock, scenarioActivityRepository: ScenarioActivityRepository, migrations: ProcessingTypeDataProvider[ProcessMigrations, _], ): DBProcessRepository = - new DBProcessRepository(dbRef, scenarioActivityRepository, migrations.mapValues(_.version)) + new DBProcessRepository(dbRef, clock, scenarioActivityRepository, migrations.mapValues(_.version)) final case class CreateProcessAction( processName: ProcessName, @@ -90,6 +91,7 @@ trait ProcessRepository[F[_]] { class DBProcessRepository( protected val dbRef: DbRef, + clock: Clock, scenarioActivityRepository: ScenarioActivityRepository, modelVersion: ProcessingTypeDataProvider[Int, _], ) extends ProcessRepository[DB] @@ -172,6 +174,7 @@ class DBProcessRepository( comment = ScenarioComment.Available( comment = comment.value, lastModifiedByUserName = UserName(loggedUser.username), + lastModifiedAt = clock.instant(), ) ) ) 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/ScenarioActionRepository.scala similarity index 99% rename from designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/ProcessActionRepository.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/ScenarioActionRepository.scala index f0bbea7c1f5..e4e70de5c1c 100644 --- 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/ScenarioActionRepository.scala @@ -22,7 +22,7 @@ import java.util.UUID import scala.concurrent.ExecutionContext //TODO: Add missing methods: markProcessAsDeployed and markProcessAsCancelled -trait ProcessActionRepository { +trait ScenarioActionRepository { def markProcessAsArchived( processId: ProcessId, @@ -57,13 +57,13 @@ trait ProcessActionRepository { } -class DbProcessActionRepository( +class DbScenarioActionRepository( protected val dbRef: DbRef, buildInfos: ProcessingTypeDataProvider[Map[String, String], _] )(implicit ec: ExecutionContext) extends DbioRepository with NuTables - with ProcessActionRepository + with ScenarioActionRepository with LazyLogging { import profile.api._ @@ -250,6 +250,7 @@ class DbProcessActionRepository( 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), 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 index 6b57b20356d..0cfbc564bad 100644 --- 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 @@ -23,11 +23,11 @@ import pl.touk.nussknacker.ui.security.api.LoggedUser import pl.touk.nussknacker.ui.statistics.{AttachmentsTotal, CommentsTotal} import java.sql.Timestamp -import java.time.Instant +import java.time.{Clock, Instant} import scala.concurrent.ExecutionContext import scala.util.Try -class DbScenarioActivityRepository(override protected val dbRef: DbRef)( +class DbScenarioActivityRepository(override protected val dbRef: DbRef, clock: Clock)( implicit executionContext: ExecutionContext, ) extends DbioRepository with NuTables @@ -42,24 +42,6 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( doFindActivities(scenarioId).map(_.map(_._2)) } - 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) - } - } - } - def addActivity( scenarioActivity: ScenarioActivity, )(implicit user: LoggedUser): DB[ScenarioActivityId] = { @@ -71,16 +53,18 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( 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 = Instant.now(), + date = now, scenarioVersion = Some(ScenarioVersion(processVersionId.value)), comment = ScenarioComment.Available( comment = comment, lastModifiedByUserName = UserName(user.username), + lastModifiedAt = now, ) ), ).map(_.activityId) @@ -95,7 +79,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( rowId = rowId, activityDoesNotExistError = ModifyCommentError.ActivityDoesNotExist, validateCurrentValue = validateCommentExists(scenarioId), - modify = _.copy(comment = Some(comment), lastModifiedByUserName = Some(user.username)), + modify = doEditComment(comment), couldNotModifyError = ModifyCommentError.CouldNotModifyComment, ) } @@ -109,7 +93,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( activityId = activityId, activityDoesNotExistError = ModifyCommentError.ActivityDoesNotExist, validateCurrentValue = validateCommentExists(scenarioId), - modify = _.copy(comment = Some(comment), lastModifiedByUserName = Some(user.username)), + modify = doEditComment(comment), couldNotModifyError = ModifyCommentError.CouldNotModifyComment, ) } @@ -122,7 +106,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( rowId = rowId, activityDoesNotExistError = ModifyCommentError.ActivityDoesNotExist, validateCurrentValue = validateCommentExists(scenarioId), - modify = _.copy(comment = None, lastModifiedByUserName = Some(user.username)), + modify = doDeleteComment, couldNotModifyError = ModifyCommentError.CouldNotModifyComment, ) } @@ -135,21 +119,15 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( activityId = activityId, activityDoesNotExistError = ModifyCommentError.ActivityDoesNotExist, validateCurrentValue = validateCommentExists(scenarioId), - modify = _.copy(comment = None, lastModifiedByUserName = Some(user.username)), + modify = doDeleteComment, couldNotModifyError = ModifyCommentError.CouldNotModifyComment, ) } - private def validateCommentExists(scenarioId: ProcessId)(entity: ScenarioActivityEntityData) = { - for { - _ <- Either.cond(entity.scenarioId == scenarioId, (), ModifyCommentError.CommentDoesNotExist) - _ <- entity.comment.toRight(ModifyCommentError.CommentDoesNotExist) - } yield () - } - def addAttachment( attachmentToAdd: AttachmentToAdd )(implicit user: LoggedUser): DB[ScenarioActivityId] = { + val now = clock.instant() for { attachment <- attachmentInsertQuery += AttachmentEntityData( id = -1L, @@ -160,19 +138,20 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( user = user.username, impersonatedByIdentity = user.impersonatingUserId, impersonatedByUsername = user.impersonatingUserName, - createDate = Timestamp.from(Instant.now()) + createDate = Timestamp.from(now) ) activity <- insertActivity( ScenarioActivity.AttachmentAdded( scenarioId = ScenarioId(attachmentToAdd.scenarioId.value), scenarioActivityId = ScenarioActivityId.random, user = toUser(user), - date = Instant.now(), + 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, ) ), ) @@ -212,6 +191,42 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( ) } + 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, @@ -221,8 +236,8 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( for { scenarioVersion <- scenarioActivity.scenarioVersion content <- comment match { - case ScenarioComment.Available(comment, _) => Some(comment) - case ScenarioComment.Deleted(_) => None + case ScenarioComment.Available(comment, _, _) => Some(comment) + case ScenarioComment.Deleted(_, _) => None } } yield Legacy.Comment( id = id, @@ -253,7 +268,8 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( toComment( id, activity, - ScenarioComment.Available(s"Rename: [${activity.oldName}] -> [${activity.newName}]", UserName("")), + ScenarioComment + .Available(s"Rename: [${activity.oldName}] -> [${activity.newName}]", UserName(""), activity.date), None ) case activity: ScenarioActivity.CommentAdded => @@ -277,17 +293,6 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( } } - 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 toUser(loggedUser: LoggedUser) = { ScenarioUser( id = Some(UserId(loggedUser.id)), @@ -317,7 +322,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( ): DB[Either[ERROR, Unit]] = { modifyActivity[ScenarioActivityId, ERROR]( key = activityId, - pullRows = activityByIdCompiled(_).result.headOption, + fetchActivity = activityByIdCompiled(_).result.headOption, updateRow = (id: ScenarioActivityId, updatedEntity) => activityByIdCompiled(id).update(updatedEntity), activityDoesNotExistError = activityDoesNotExistError, validateCurrentValue = validateCurrentValue, @@ -335,7 +340,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( ): DB[Either[ERROR, Unit]] = { modifyActivity[Long, ERROR]( key = rowId, - pullRows = activityByRowIdCompiled(_).result.headOption, + fetchActivity = activityByRowIdCompiled(_).result.headOption, updateRow = (id: Long, updatedEntity) => activityByRowIdCompiled(id).update(updatedEntity), activityDoesNotExistError = activityDoesNotExistError, validateCurrentValue = validateCurrentValue, @@ -346,7 +351,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( private def modifyActivity[KEY, ERROR]( key: KEY, - pullRows: KEY => DB[Option[ScenarioActivityEntityData]], + fetchActivity: KEY => DB[Option[ScenarioActivityEntityData]], updateRow: (KEY, ScenarioActivityEntityData) => DB[Int], activityDoesNotExistError: ERROR, validateCurrentValue: ScenarioActivityEntityData => Either[ERROR, Unit], @@ -354,10 +359,10 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( couldNotModifyError: ERROR, ): DB[Either[ERROR, Unit]] = { val action = for { - dataPulled <- pullRows(key) + fetchedActivity <- fetchActivity(key) result <- { val modifiedEntity = for { - entity <- dataPulled.toRight(activityDoesNotExistError) + entity <- fetchedActivity.toRight(activityDoesNotExistError) _ <- validateCurrentValue(entity) modifiedEntity = modify(entity) } yield modifiedEntity @@ -389,6 +394,32 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( } } + 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, @@ -399,6 +430,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( 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 @@ -428,7 +460,8 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( impersonatedByUserId = scenarioActivity.user.impersonatedByUserId.map(_.value), impersonatedByUserName = scenarioActivity.user.impersonatedByUserName.map(_.value), lastModifiedByUserName = lastModifiedByUserName, - createdAt = Timestamp.from(Instant.now()), + lastModifiedAt = Some(now), + createdAt = now, scenarioVersion = scenarioActivity.scenarioVersion, comment = comment, attachmentId = attachmentId, @@ -442,30 +475,30 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( private def comment(scenarioComment: ScenarioComment): Option[String] = { scenarioComment match { - case ScenarioComment.Available(comment, _) => Some(comment.value) - case ScenarioComment.Deleted(_) => None + 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 + 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) => + case ScenarioAttachment.Available(_, _, lastModifiedByUserName, _) => Some(lastModifiedByUserName.value) - case ScenarioAttachment.Deleted(_, deletedByUserName) => + case ScenarioAttachment.Deleted(_, deletedByUserName, _) => Some(deletedByUserName.value) } Some(userName.value) } - def toEntity(scenarioActivity: ScenarioActivity): ScenarioActivityEntityData = { + private def toEntity(scenarioActivity: ScenarioActivity): ScenarioActivityEntityData = { scenarioActivity match { case _: ScenarioActivity.ScenarioCreated => createEntity(scenarioActivity)() @@ -510,8 +543,8 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( ) 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)) + case ScenarioAttachment.Available(id, filename, _, _) => (Some(id.value), Some(filename.value)) + case ScenarioAttachment.Deleted(filename, _, _) => (None, Some(filename.value)) } createEntity(scenarioActivity)( attachmentId = attachmentId, @@ -589,12 +622,20 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( 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)) + ScenarioComment.Available( + comment = comment, + lastModifiedByUserName = UserName(lastModifiedByUserName), + lastModifiedAt = lastModifiedAt.toInstant + ) case None => - ScenarioComment.Deleted(deletedByUserName = UserName(lastModifiedByUserName)) + ScenarioComment.Deleted( + deletedByUserName = UserName(lastModifiedByUserName), + deletedAt = lastModifiedAt.toInstant + ) } } } @@ -603,18 +644,21 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( 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) + lastModifiedByUserName = UserName(lastModifiedByUserName), + lastModifiedAt = lastModifiedAt.toInstant, ) case None => ScenarioAttachment.Deleted( attachmentFilename = AttachmentFilename(filename), - deletedByUserName = UserName(lastModifiedByUserName) + deletedByUserName = UserName(lastModifiedByUserName), + deletedAt = lastModifiedAt.toInstant, ) } } @@ -624,7 +668,7 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef)( entity.additionalProperties.properties.get(name).toRight(s"Missing additional property $name") } - def fromEntity(entity: ScenarioActivityEntityData): Either[String, (Long, ScenarioActivity)] = { + private def fromEntity(entity: ScenarioActivityEntityData): Either[String, (Long, ScenarioActivity)] = { entity.activityType match { case ScenarioActivityType.ScenarioCreated => ScenarioActivity 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 792997276ed..f5a20b469c8 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 @@ -152,12 +152,13 @@ class AkkaHttpBasedRouteProvider( val modelBuildInfo = processingTypeDataProvider.mapValues(_.designerModelData.modelData.buildInfo) implicit val implicitDbioRunner: DBIOActionRunner = dbioRunner - val scenarioActivityRepository = new DbScenarioActivityRepository(dbRef) - val actionRepository = new DbProcessActionRepository(dbRef, modelBuildInfo) + val scenarioActivityRepository = new DbScenarioActivityRepository(dbRef, designerClock) + val actionRepository = new DbScenarioActionRepository(dbRef, modelBuildInfo) val processRepository = DBFetchingProcessRepository.create(dbRef, actionRepository) // 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) - val writeProcessRepository = ProcessRepository.create(dbRef, scenarioActivityRepository, migrations) + val writeProcessRepository = + ProcessRepository.create(dbRef, designerClock, scenarioActivityRepository, migrations) val fragmentRepository = new DefaultFragmentRepository(futureProcessRepository) val fragmentResolver = new FragmentResolver(fragmentRepository) @@ -234,12 +235,12 @@ class AkkaHttpBasedRouteProvider( // correct classloader and that won't cause further delays during handling requests processingTypeDataProvider.reloadAll().unsafeRunSync() - val processActivityRepository = new DbScenarioActivityRepository(dbRef) + val processActivityRepository = new DbScenarioActivityRepository(dbRef, designerClock) val authenticationResources = AuthenticationResources(resolvedConfig, getClass.getClassLoader, sttpBackend) val authManager = new AuthManager(authenticationResources) - Initialization.init(migrations, dbRef, processRepository, processActivityRepository, environment) + Initialization.init(migrations, dbRef, designerClock, processRepository, processActivityRepository, environment) val newProcessPreparer = processingTypeDataProvider.mapValues { processingTypeData => new NewProcessPreparer( @@ -402,7 +403,8 @@ class AkkaHttpBasedRouteProvider( featureTogglesConfig.deploymentCommentSettings, scenarioActivityRepository, deploymentService, - dbioRunner + dbioRunner, + designerClock, ) new DeploymentApiHttpService(authManager, activityService, deploymentService) } diff --git a/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala b/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala index 5eb77120f3d..e521edef150 100644 --- a/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala +++ b/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala @@ -1,6 +1,9 @@ package db.migration -import db.migration.V1_055__CreateScenarioActivitiesDefinition.ScenarioActivityEntityData +import db.migration.V1_055__CreateScenarioActivitiesDefinition.{ + ScenarioActivitiesDefinitions, + ScenarioActivityEntityData +} import db.migration.V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition._ import io.circe.syntax.EncoderOps import org.scalatest.freespec.AnyFreeSpecLike @@ -44,16 +47,20 @@ class V1_056__MigrateActionsAndCommentsToScenarioActivities val migration = new Migration(HsqldbProfile) val processActionsDefinitions = new ProcessActionsDefinitions(profile) val commentsDefinitions = new CommentsDefinitions(profile) + val activitiesDefinitions = new ScenarioActivitiesDefinitions(profile) val now: Timestamp = Timestamp.from(Instant.now) val user = "John Doe" val versionId = VersionId(5L) - val actionId = UUID.randomUUID() - val commentId = 765L - val processInsertQuery = processesTable returning processesTable.map(_.id) into ((item, id) => item.copy(id = id)) + val processInsertQuery = processesTable returning + processesTable.map(_.id) into ((item, id) => item.copy(id = id)) + val commentInsertQuery = commentsDefinitions.table returning + commentsDefinitions.table.map(_.id) into ((item, id) => item.copy(id = id)) + val actionInsertQuery = processActionsDefinitions.table returning + processActionsDefinitions.table.map(_.id) into ((item, id) => item.copy(id = id)) - val processEntity = ProcessEntityData( + def processEntity() = ProcessEntityData( id = ProcessId(-1L), name = ProcessName("2024_Q3_6917_NETFLIX"), processCategory = "test-category", @@ -91,19 +98,19 @@ class V1_056__MigrateActionsAndCommentsToScenarioActivities componentsUsages = Some(ScenarioComponentsUsages.Empty), ) - def commentEntity(processEntity: ProcessEntityData) = CommentEntityData( + def commentEntity(processEntity: ProcessEntityData, commentId: Long) = CommentEntityData( id = commentId, processId = processEntity.id.value, processVersionId = versionId.value, - content = "Very important change", + content = s"Very important change $commentId", user = user, impersonatedByIdentity = None, impersonatedByUsername = None, createDate = now, ) - def processActionEntity(processEntity: ProcessEntityData) = ProcessActionEntityData( - id = actionId, + def processActionEntity(processEntity: ProcessEntityData, commentId: Long) = ProcessActionEntityData( + id = UUID.randomUUID(), processId = processEntity.id.value, processVersionId = Some(versionId.value), user = user, @@ -118,38 +125,52 @@ class V1_056__MigrateActionsAndCommentsToScenarioActivities buildInfo = None ) - val dbOperations = for { - process <- processInsertQuery += processEntity - _ <- processVersionsTable += processVersionEntity(process) - _ <- commentsDefinitions.table += commentEntity(process) - _ <- processActionsDefinitions.table += processActionEntity(process) - result <- migration.migrateActions - } yield (process, result._1) - - val (createdProcess, insertedActivities) = Await.result(runner.run(dbOperations), Duration.Inf) - insertedActivities shouldBe - List( - ScenarioActivityEntityData( - id = -1, - activityType = "SCENARIO_DEPLOYED", - scenarioId = createdProcess.id.value, - activityId = actionId, - userId = None, - userName = user, - impersonatedByUserId = None, - impersonatedByUserName = None, - lastModifiedByUserName = Some(user), - createdAt = now, - scenarioVersion = Some(versionId.value), - comment = Some("Very important change"), - attachmentId = None, - finishedAt = None, - state = Some("IN_PROGRESS"), - errorMessage = None, - buildInfo = None, - additionalProperties = AdditionalProperties.empty.properties.asJson.noSpaces, - ) - ) + val (createdProcess, migratedCount, actionsBeingMigrated, activitiesAfterMigration) = Await.result( + runner.run( + for { + process <- processInsertQuery += processEntity() + _ <- processVersionsTable += processVersionEntity(process) + comments <- commentInsertQuery ++= List.range(1L, 100001L).map(idx => commentEntity(process, idx)) + actions <- actionInsertQuery ++= comments.map(comment => processActionEntity(process, comment.id)) + migratedCount <- migration.migrateActions + activities <- activitiesDefinitions.scenarioActivitiesTable.result + } yield (process, migratedCount, actions, activities) + ), + Duration.Inf + ) + + actionsBeingMigrated.length shouldBe 100000 + migratedCount shouldBe 100000 + activitiesAfterMigration.length shouldBe 100000 + + val headActivity = + activitiesAfterMigration.head + val expectedOldCommentIdForHeadActivity = + headActivity.comment.flatMap(_.filter(_.isDigit).toLongOption).get + val expectedActionIdForHeadActivity = + actionsBeingMigrated.find(_.commentId.contains(expectedOldCommentIdForHeadActivity)).map(_.id).get + + headActivity shouldBe ScenarioActivityEntityData( + id = 1L, + 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(versionId.value), + 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, + ) } } 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 d11685798cf..48bd59b4e4e 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} 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 05ee7cd647e..391d55c9dec 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 @@ -55,14 +57,14 @@ 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 @@ -86,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 scenarioActivityRepository: ScenarioActivityRepository = newScenarioActivityRepository(testDbRef) + protected val scenarioActivityRepository: ScenarioActivityRepository = newScenarioActivityRepository(testDbRef, clock) protected val processChangeListener = new TestProcessChangeListener() 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 34d0d94938d..f7319819a63 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 @@ -21,24 +21,27 @@ import pl.touk.nussknacker.ui.process.repository.activities.DbScenarioActivityRe 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, mapProcessingTypeDataProvider(Map("engine-version" -> "0.1")) ) with DbioRepository private val writeScenarioRepository: DBProcessRepository = new DBProcessRepository( dbRef, - new DbScenarioActivityRepository(dbRef), + clock, + new DbScenarioActivityRepository(dbRef, clock), mapProcessingTypeDataProvider(1) ) 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 301c677293f..15a6f5557e2 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 @@ -144,7 +144,7 @@ object TestFactory { def newDummyDBIOActionRunner(): DBIOActionRunner = newDBIOActionRunner(dummyDbRef) - def newScenarioActivityRepository(dbRef: DbRef) = new DbScenarioActivityRepository(dbRef) + def newScenarioActivityRepository(dbRef: DbRef, clock: Clock) = new DbScenarioActivityRepository(dbRef, clock) def newFutureFetchingScenarioRepository(dbRef: DbRef) = new DBFetchingProcessRepository[Future](dbRef, newActionProcessRepository(dbRef)) with BasicRepository @@ -152,15 +152,16 @@ object TestFactory { def newFetchingProcessRepository(dbRef: DbRef) = new DBFetchingProcessRepository[DB](dbRef, newActionProcessRepository(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, - newScenarioActivityRepository(dbRef), + clock, + newScenarioActivityRepository(dbRef, clock), mapProcessingTypeDataProvider(modelVersions.map(Streaming.stringify -> _).toList: _*), ) def newDummyWriteProcessRepository(): DBProcessRepository = - newWriteProcessRepository(dummyDbRef) + newWriteProcessRepository(dummyDbRef, Clock.systemUTC()) def newScenarioGraphVersionService(dbRef: DbRef) = new ScenarioGraphVersionService( newScenarioGraphVersionRepository(dbRef), @@ -175,12 +176,12 @@ object TestFactory { new DefaultFragmentRepository(newFutureFetchingScenarioRepository(dbRef)) def newActionProcessRepository(dbRef: DbRef) = - new DbProcessActionRepository( + new DbScenarioActionRepository( dbRef, mapProcessingTypeDataProvider(Streaming.stringify -> buildInfo) ) with DbioRepository - def newDummyActionRepository(): DbProcessActionRepository = + def newDummyActionRepository(): DbScenarioActionRepository = newActionProcessRepository(dummyDbRef) def newScenarioMetadataRepository(dbRef: DbRef) = new ScenarioMetadataRepository(dbRef) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DeploymentApiHttpServiceDeploymentCommentSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DeploymentApiHttpServiceDeploymentCommentSpec.scala index 5fab28dc706..aedab7354bb 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DeploymentApiHttpServiceDeploymentCommentSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DeploymentApiHttpServiceDeploymentCommentSpec.scala @@ -66,8 +66,6 @@ class DeploymentApiHttpServiceDeploymentCommentSpec ) } - testDbRef.db - "The deployment requesting endpoint" - { "With validationPattern configured in deploymentCommentSettings" - { "When no deployment comment is passed should" - { 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 35eecf5b16c..23e04341ae9 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 @@ -7,16 +7,17 @@ import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} import pl.touk.nussknacker.engine.api.process.ProcessName import pl.touk.nussknacker.test.PatientScalaFutures 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,20 +35,20 @@ abstract class InitializationOnDbItSpec private val migrations = mapProcessingTypeDataProvider("streaming" -> new TestMigrations(1, 2)) - private lazy val scenarioActivityRepository = TestFactory.newScenarioActivityRepository(testDbRef) + private lazy val scenarioActivityRepository = TestFactory.newScenarioActivityRepository(testDbRef, clock) private lazy val scenarioRepository = TestFactory.newFetchingProcessRepository(testDbRef) 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, scenarioActivityRepository, "env1") + Initialization.init(migrations, testDbRef, clock, scenarioRepository, scenarioActivityRepository, "env1") dbioRunner .runInTransaction( @@ -66,7 +67,7 @@ abstract class InitializationOnDbItSpec saveSampleProcess(ProcessName(s"id$id")) } - Initialization.init(migrations, testDbRef, scenarioRepository, scenarioActivityRepository, "env1") + Initialization.init(migrations, testDbRef, clock, scenarioRepository, scenarioActivityRepository, "env1") dbioRunner .runInTransaction( @@ -84,6 +85,7 @@ abstract class InitializationOnDbItSpec Initialization.init( mapProcessingTypeDataProvider("streaming" -> new TestMigrations(1, 2, 5)), testDbRef, + clock, scenarioRepository, scenarioActivityRepository, "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 174ae740bea..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,10 +61,10 @@ 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 writeProcessRepository = TestFactory.newWriteProcessRepository(testDbRef, clock) private val actionRepository = - new DbProcessActionRepository( + new DbScenarioActionRepository( testDbRef, 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/deployment/DeploymentServiceSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/deployment/DeploymentServiceSpec.scala index 60b6e0aceda..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} @@ -49,6 +50,7 @@ class DeploymentServiceSpec with BeforeAndAfterEach with BeforeAndAfterAll with WithHsqlDbTesting + with WithClock with EitherValuesDetailedMessage { import VersionId._ @@ -63,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 = newScenarioActivityRepository(testDbRef) + private val activityRepository = newScenarioActivityRepository(testDbRef, clock) private val processingTypeDataProvider: ProcessingTypeDataProvider[DeploymentManager, Nothing] = new ProcessingTypeDataProvider[DeploymentManager, Nothing] { 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 9bd1b58d4e0..f660c4566fb 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,6 +12,7 @@ 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 @@ -37,21 +38,22 @@ class DBFetchingProcessRepositorySpec with BeforeAndAfterEach with BeforeAndAfterAll with WithHsqlDbTesting + with WithClock with PatientScalaFutures { private val dbioRunner = DBIOActionRunner(testDbRef) - private val activities = new DbScenarioActivityRepository(testDbRef) + private val activities = new DbScenarioActivityRepository(testDbRef, clock) private val writingRepo = - new DBProcessRepository(testDbRef, activities, mapProcessingTypeDataProvider("Streaming" -> 0)) { + new DBProcessRepository(testDbRef, clock, activities, mapProcessingTypeDataProvider("Streaming" -> 0)) { override protected def now: Instant = currentTime } private var currentTime: Instant = Instant.now() private val actions = - new DbProcessActionRepository( + new DbScenarioActionRepository( testDbRef, ProcessingTypeDataProvider.withEmptyCombinedData(Map.empty) ) 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/ProcessActivity.scala b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala index 2b3d501c0f2..b78b6e3ba15 100644 --- a/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala +++ b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala @@ -32,10 +32,12 @@ object ScenarioComment { final case class Available( comment: String, lastModifiedByUserName: UserName, + lastModifiedAt: Instant, ) extends ScenarioComment final case class Deleted( deletedByUserName: UserName, + deletedAt: Instant, ) extends ScenarioComment } @@ -48,11 +50,13 @@ object ScenarioAttachment { attachmentId: AttachmentId, attachmentFilename: AttachmentFilename, lastModifiedByUserName: UserName, + lastModifiedAt: Instant, ) extends ScenarioAttachment final case class Deleted( attachmentFilename: AttachmentFilename, - deletedByUserName: UserName + deletedByUserName: UserName, + deletedAt: Instant, ) extends ScenarioAttachment final case class AttachmentId(value: Long) extends AnyVal diff --git a/utils/test-utils/src/main/resources/logback-test.xml b/utils/test-utils/src/main/resources/logback-test.xml index f68a6a838af..8dd342e8b57 100644 --- a/utils/test-utils/src/main/resources/logback-test.xml +++ b/utils/test-utils/src/main/resources/logback-test.xml @@ -11,8 +11,6 @@ - - From 6872380416a7c5a5da18d719ae69a29c1bac5404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 16 Sep 2024 23:44:14 +0200 Subject: [PATCH 31/43] HTTP service fixes --- .../ui/api/ScenarioActivityApiHttpService.scala | 12 +++++------- .../api/description/scenarioActivity/Dtos.scala | 13 ++++++++----- .../description/scenarioActivity/Endpoints.scala | 15 +++++++++------ .../description/scenarioActivity/Examples.scala | 13 +++++++++++-- .../scenarioActivity/InputOutput.scala | 4 ---- 5 files changed, 33 insertions(+), 24 deletions(-) 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 667b4338d19..3a69c619202 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 @@ -12,6 +12,7 @@ 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 @@ -179,9 +180,6 @@ class ScenarioActivityApiHttpService( } } - 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), @@ -461,7 +459,7 @@ class ScenarioActivityApiHttpService( dbioActionRunner.run( scenarioActivityRepository.editComment(scenarioId, request.commentId, request.commentContent) ) - ).leftMap(_ => NoComment(request.commentId.toString)) + ).leftMap(_ => NoComment(request.commentId)) private def editComment(request: EditCommentRequest, scenarioId: ProcessId)( implicit loggedUser: LoggedUser @@ -474,14 +472,14 @@ class ScenarioActivityApiHttpService( request.commentContent ) ) - ).leftMap(_ => NoComment(request.scenarioActivityId.toString)) + ).leftMap(_ => NoActivity(request.scenarioActivityId)) private def deleteComment(request: DeprecatedDeleteCommentRequest, scenarioId: ProcessId)( implicit loggedUser: LoggedUser ): EitherT[Future, ScenarioActivityError, Unit] = EitherT( dbioActionRunner.run(scenarioActivityRepository.deleteComment(scenarioId, request.commentId)) - ).leftMap(_ => NoComment(request.commentId.toString)) + ).leftMap(_ => NoComment(request.commentId)) private def deleteComment(request: DeleteCommentRequest, scenarioId: ProcessId)( implicit loggedUser: LoggedUser @@ -490,7 +488,7 @@ class ScenarioActivityApiHttpService( dbioActionRunner.run( scenarioActivityRepository.deleteComment(scenarioId, ScenarioActivityId(request.scenarioActivityId)) ) - ).leftMap(_ => NoComment(request.scenarioActivityId.toString)) + ).leftMap(_ => NoActivity(request.scenarioActivityId)) private def fetchAttachments(scenarioId: ProcessId): EitherT[Future, ScenarioActivityError, ScenarioAttachments] = { EitherT 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 c74f63ab9fe..839faa5e3e6 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 @@ -531,19 +531,22 @@ 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(scenarioActivityId: 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") implicit val noCommentCodec: Codec[String, NoComment, CodecFormat.TextPlain] = BaseEndpointDefinitions.toTextPlainCodecSerializationOnly[NoComment](e => - s"Unable to delete comment with id: ${e.scenarioActivityId}" + 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}" ) } 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..de00205ab59 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 @@ -230,7 +234,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), - ) ) } From 34b26bce0f47c4907d0ec7c3cabdab8d67647937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 17 Sep 2024 10:32:55 +0200 Subject: [PATCH 32/43] Scala 2.12 fix --- .../V1_056__MigrateActionsAndCommentsToScenarioActivities.scala | 2 +- utils/test-utils/src/main/resources/logback-test.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala b/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala index e521edef150..94d31cc7722 100644 --- a/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala +++ b/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala @@ -146,7 +146,7 @@ class V1_056__MigrateActionsAndCommentsToScenarioActivities val headActivity = activitiesAfterMigration.head val expectedOldCommentIdForHeadActivity = - headActivity.comment.flatMap(_.filter(_.isDigit).toLongOption).get + headActivity.comment.map(_.filter(_.isDigit).toLong).get val expectedActionIdForHeadActivity = actionsBeingMigrated.find(_.commentId.contains(expectedOldCommentIdForHeadActivity)).map(_.id).get diff --git a/utils/test-utils/src/main/resources/logback-test.xml b/utils/test-utils/src/main/resources/logback-test.xml index 8dd342e8b57..2407b450eb6 100644 --- a/utils/test-utils/src/main/resources/logback-test.xml +++ b/utils/test-utils/src/main/resources/logback-test.xml @@ -57,7 +57,7 @@ - + From 212567ad938082b8235aab19d42e057cce70c4c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 17 Sep 2024 11:06:57 +0200 Subject: [PATCH 33/43] API swagger and migration fixes --- ...grateActionsAndCommentsToScenarioActivities.scala | 2 +- docs-internal/api/nu-designer-openapi.yaml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala b/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala index 94d31cc7722..3c99aef31c5 100644 --- a/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala +++ b/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala @@ -151,7 +151,7 @@ class V1_056__MigrateActionsAndCommentsToScenarioActivities actionsBeingMigrated.find(_.commentId.contains(expectedOldCommentIdForHeadActivity)).map(_.id).get headActivity shouldBe ScenarioActivityEntityData( - id = 1L, + id = headActivity.id, activityType = "SCENARIO_DEPLOYED", scenarioId = createdProcess.id.value, activityId = expectedActionIdForHeadActivity, diff --git a/docs-internal/api/nu-designer-openapi.yaml b/docs-internal/api/nu-designer-openapi.yaml index 6821cd3bf28..c21445f64d8 100644 --- a/docs-internal/api/nu-designer-openapi.yaml +++ b/docs-internal/api/nu-designer-openapi.yaml @@ -2951,8 +2951,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: @@ -3041,8 +3041,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: @@ -3220,8 +3220,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: From e6d67a673d200f034a376b3f833706687886901e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 17 Sep 2024 13:52:47 +0200 Subject: [PATCH 34/43] Fix migration issues --- ...__CreateScenarioActivitiesDefinition.scala | 59 +++++-------------- ...mmentsToScenarioActivitiesDefinition.scala | 18 +++--- .../ScenarioActivityEntityFactory.scala | 2 +- .../test/utils/OpenAPIExamplesValidator.scala | 8 +-- 4 files changed, 25 insertions(+), 62 deletions(-) diff --git a/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala index 0ccdf70460a..573bfc523e8 100644 --- a/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala +++ b/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala @@ -3,6 +3,7 @@ package db.migration import com.typesafe.scalalogging.LazyLogging import db.migration.V1_055__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 @@ -16,10 +17,13 @@ trait V1_055__CreateScenarioActivitiesDefinition extends SlickMigration with Laz private val definitions = new ScenarioActivitiesDefinitions(profile) - override def migrateActions: DBIOAction[Any, NoStream, _ <: Effect] = { + override def migrateActions: DBIOAction[Any, NoStream, Effect.All] = { logger.info("Starting migration V1_055__CreateScenarioActivitiesDefinition") - definitions.scenarioActivitiesTable.schema.create - .map(_ => logger.info("Execution finished for migration V1_055__CreateScenarioActivitiesDefinition")) + for { + _ <- definitions.scenarioActivitiesTable.schema.create + _ <- + sqlu"""ALTER TABLE "scenario_activities" ADD CONSTRAINT scenario_id_fk FOREIGN KEY ("scenario_id") REFERENCES "processes" ("id");""" + } yield logger.info("Execution finished for migration V1_055__CreateScenarioActivitiesDefinition") } } @@ -39,7 +43,7 @@ object V1_055__CreateScenarioActivitiesDefinition { def scenarioId: Rep[Long] = column[Long]("scenario_id", NotNull) - def activityId: Rep[UUID] = column[UUID]("activity_id", NotNull) + def activityId: Rep[UUID] = column[UUID]("activity_id", NotNull, O.Unique) def userId: Rep[Option[String]] = column[Option[String]]("user_id") @@ -71,26 +75,11 @@ object V1_055__CreateScenarioActivitiesDefinition { def additionalProperties: Rep[String] = column[String]("additional_properties", NotNull) - def tuple: ( - Rep[String], - Rep[Long], - Rep[UUID], - Rep[Option[String]], - Rep[String], - Rep[Option[String]], - Rep[Option[String]], - Rep[Option[String]], - Rep[Option[Timestamp]], - Rep[Timestamp], - Rep[Option[Long]], - Rep[Option[String]], - Rep[Option[Long]], - Rep[Option[Timestamp]], - Rep[Option[String]], - Rep[Option[String]], - Rep[Option[String]], - Rep[String] - ) = ( + 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, @@ -112,27 +101,7 @@ object V1_055__CreateScenarioActivitiesDefinition { ) override def * = - ( - id, - activityType, - scenarioId, - activityId, - userId, - userName, - impersonatedByUserId, - impersonatedByUserName, - lastModifiedByUserName, - lastModifiedAt, - createdAt, - scenarioVersion, - comment, - attachmentId, - performedAt, - state, - errorMessage, - buildInfo, - additionalProperties, - ) <> ( + (id :: tupleWithoutAutoIncId.productElements).tupled <> ( ScenarioActivityEntityData.apply _ tupled, ScenarioActivityEntityData.unapply ) diff --git a/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala index 4e03b764219..9ffc1240ce0 100644 --- a/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala +++ b/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala @@ -3,8 +3,6 @@ package db.migration import com.typesafe.scalalogging.LazyLogging import db.migration.V1_055__CreateScenarioActivitiesDefinition.ScenarioActivitiesDefinitions import db.migration.V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.Migration -import pl.touk.nussknacker.engine.api.deployment.ScenarioActionName -import pl.touk.nussknacker.engine.management.periodic.InstantBatchCustomAction import pl.touk.nussknacker.ui.db.entity.{ScenarioActivityEntityFactory, ScenarioActivityType} import pl.touk.nussknacker.ui.db.migration.SlickMigration import slick.jdbc.JdbcProfile @@ -64,26 +62,26 @@ object V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends L } // Slick generates single "insert from select" query and operation is performed solely on db - scenarioActivitiesDefinitions.scenarioActivitiesTable.map(_.tuple).forceInsertQuery(insertQuery) + scenarioActivitiesDefinitions.scenarioActivitiesTable.map(_.tupleWithoutAutoIncId).forceInsertQuery(insertQuery) } def activityType(actionNameRep: Rep[String]): Rep[String] = { val customActionPrefix = s"CUSTOM_ACTION_[" val customActionSuffix = "]" Case - .If(actionNameRep === ScenarioActionName.Deploy.value) + .If(actionNameRep === "DEPLOY") .Then(ScenarioActivityType.ScenarioDeployed.entryName) - .If(actionNameRep === ScenarioActionName.Cancel.value) + .If(actionNameRep === "CANCEL") .Then(ScenarioActivityType.ScenarioCanceled.entryName) - .If(actionNameRep === ScenarioActionName.Archive.value) + .If(actionNameRep === "ARCHIVE") .Then(ScenarioActivityType.ScenarioArchived.entryName) - .If(actionNameRep === ScenarioActionName.UnArchive.value) + .If(actionNameRep === "UNARCHIVE") .Then(ScenarioActivityType.ScenarioUnarchived.entryName) - .If(actionNameRep === ScenarioActionName.Pause.value) + .If(actionNameRep === "PAUSE") .Then(ScenarioActivityType.ScenarioPaused.entryName) - .If(actionNameRep === ScenarioActionName.Rename.value) + .If(actionNameRep === "RENAME") .Then(ScenarioActivityType.ScenarioNameChanged.entryName) - .If(actionNameRep === InstantBatchCustomAction.name.value) + .If(actionNameRep === "run now") .Then(ScenarioActivityType.PerformedSingleExecution.entryName) .Else(actionNameRep.reverseString.++(customActionPrefix.reverse).reverseString.++(customActionSuffix)) } 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 index fd4056d2459..a5b58359ce7 100644 --- 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 @@ -31,7 +31,7 @@ trait ScenarioActivityEntityFactory extends BaseEntityFactory { def scenarioId: Rep[ProcessId] = column[ProcessId]("scenario_id", NotNull) - def activityId: Rep[ScenarioActivityId] = column[ScenarioActivityId]("activity_id", NotNull) + def activityId: Rep[ScenarioActivityId] = column[ScenarioActivityId]("activity_id", NotNull, O.Unique) def userId: Rep[Option[String]] = column[Option[String]]("user_id") diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/OpenAPIExamplesValidator.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/OpenAPIExamplesValidator.scala index 8a39b367969..bd77d50a291 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/OpenAPIExamplesValidator.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/OpenAPIExamplesValidator.scala @@ -1,6 +1,6 @@ package pl.touk.nussknacker.test.utils -import com.networknt.schema.{InputFormat, JsonSchemaFactory, SchemaValidatorsConfig, ValidationMessage} +import com.networknt.schema.{InputFormat, JsonSchemaFactory, ValidationMessage} import io.circe.yaml.{parser => YamlParser} import io.circe.{ACursor, Json} import org.scalactic.anyvals.NonEmptyList @@ -62,14 +62,10 @@ class OpenAPIExamplesValidator private (schemaFactory: JsonSchemaFactory) { isRequest: Boolean, componentsSchemas: Map[String, Json] ): List[InvalidExample] = { - val config = new SchemaValidatorsConfig - config.setOpenAPI3StyleDiscriminators(true) for { schema <- mediaType.hcursor.downField("schema").focus.toList resolvedSchema = resolveSchemaReferences(schema, componentsSchemas) - jsonSchema = schemaFactory - .getSchema(resolvedSchema.spaces2.replace("#/components/schemas/", "#/definitions/")) - .withConfig(config) + jsonSchema = schemaFactory.getSchema(resolvedSchema.spaces2) (exampleId, exampleValue) <- mediaType.hcursor.downField("example").focus.map("example" -> _).toList ::: mediaType.hcursor.downField("examples").focusObjectFields.flatMap { case (exampleId, exampleRoot) => exampleRoot.hcursor.downField("value").focus.toList.map(exampleId -> _) From d001c5f666bcd0314d631c629f3ae41be8dfa3eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 17 Sep 2024 14:56:42 +0200 Subject: [PATCH 35/43] Fix db issues --- .../migration/V1_055__CreateScenarioActivitiesDefinition.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala index 573bfc523e8..c636bba0ecf 100644 --- a/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala +++ b/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala @@ -22,7 +22,7 @@ trait V1_055__CreateScenarioActivitiesDefinition extends SlickMigration with Laz for { _ <- definitions.scenarioActivitiesTable.schema.create _ <- - sqlu"""ALTER TABLE "scenario_activities" ADD CONSTRAINT scenario_id_fk FOREIGN KEY ("scenario_id") REFERENCES "processes" ("id");""" + 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_055__CreateScenarioActivitiesDefinition") } From 196a2dd8672b0d8a184850e4a5c74edede044603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Wed, 18 Sep 2024 10:16:56 +0200 Subject: [PATCH 36/43] Test new endpoints --- ...ioActivityApiHttpServiceBusinessSpec.scala | 175 +++++++++++++++++- 1 file changed, 166 insertions(+), 9 deletions(-) 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( From cd3fbc98202d1cbe9580c4db675185d8023e1ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Thu, 19 Sep 2024 14:34:58 +0200 Subject: [PATCH 37/43] Improve and fix migration, add tests --- ...mmentsToScenarioActivitiesDefinition.scala | 98 ++++- .../api/ScenarioActivityApiHttpService.scala | 3 +- .../description/scenarioActivity/Dtos.scala | 5 +- .../scenarioActivity/Examples.scala | 6 +- .../DbScenarioActivityRepository.scala | 95 +++-- ...tionsAndCommentsToScenarioActivities.scala | 380 +++++++++++++----- ...sActivity.scala => ScenarioActivity.scala} | 5 +- 7 files changed, 432 insertions(+), 160 deletions(-) rename extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/{ProcessActivity.scala => ScenarioActivity.scala} (98%) diff --git a/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala index 9ffc1240ce0..0825041792e 100644 --- a/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala +++ b/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala @@ -11,13 +11,14 @@ import slick.sql.SqlProfile.ColumnOption.NotNull import java.sql.Timestamp import java.util.UUID +import scala.concurrent.ExecutionContext.Implicits.global trait V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends SlickMigration with LazyLogging { import profile.api._ override def migrateActions: DBIOAction[Any, NoStream, Effect.All] = { - new Migration(profile).migrateActions + new Migration(profile).migrate } } @@ -32,32 +33,38 @@ object V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends L private val processActionsDefinitions = new ProcessActionsDefinitions(profile) private val commentsDefinitions = new CommentsDefinitions(profile) - def migrateActions: DBIOAction[Int, NoStream, Effect.All] = { + // 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), // comment - None: Option[Long], // attachmentId - processAction.performedAt, // finishedAt - processAction.state.?, // state - processAction.failureMessage, // errorMessage - None: Option[String], // buildInfo - always absent in old actions - "{}" // additionalProperties - always empty in old actions + 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 ) } @@ -65,7 +72,42 @@ object V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends L scenarioActivitiesDefinitions.scenarioActivitiesTable.map(_.tupleWithoutAutoIncId).forceInsertQuery(insertQuery) } - def activityType(actionNameRep: Rep[String]): Rep[String] = { + // 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 @@ -86,6 +128,20 @@ object V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends L .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) { 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 3a69c619202..07188e1833b 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 @@ -432,13 +432,14 @@ class ScenarioActivityApiHttpService( changes = changes, errorMessage = errorMessage, ) - case ScenarioActivity.CustomAction(_, scenarioActivityId, user, date, scenarioVersion, actionName) => + 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), ) } } 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 839faa5e3e6..545c81a0b4d 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,7 @@ object Dtos { user: String, date: Instant, scenarioVersion: Option[Long], - dateFinished: Instant, + dateFinished: Option[Instant], errorMessage: Option[String], ) extends ScenarioActivity @@ -440,7 +440,7 @@ object Dtos { user: String, date: Instant, scenarioVersion: Option[Long], - dateFinished: Instant, + dateFinished: Option[Instant], errorMessage: Option[String], ) extends ScenarioActivity @@ -462,6 +462,7 @@ object Dtos { date: Instant, scenarioVersion: Option[Long], actionName: String, + comment: ScenarioActivityComment, ) extends ScenarioActivity } 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 de00205ab59..f0dd198a737 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 @@ -180,7 +180,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 = Some("Execution error occurred"), ), ScenarioActivity.PerformedSingleExecution( @@ -188,7 +188,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.PerformedScheduledExecution( @@ -196,7 +196,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( 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 index 0cfbc564bad..bbc91c74fc4 100644 --- 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 @@ -23,7 +23,7 @@ import pl.touk.nussknacker.ui.security.api.LoggedUser import pl.touk.nussknacker.ui.statistics.{AttachmentsTotal, CommentsTotal} import java.sql.Timestamp -import java.time.{Clock, Instant} +import java.time.Clock import scala.concurrent.ExecutionContext import scala.util.Try @@ -583,12 +583,12 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef, clock: C ) case activity: ScenarioActivity.PerformedSingleExecution => createEntity(scenarioActivity)( - finishedAt = Some(Timestamp.from(activity.dateFinished)), + finishedAt = activity.dateFinished.map(Timestamp.from), errorMessage = activity.errorMessage, ) case activity: ScenarioActivity.PerformedScheduledExecution => createEntity(scenarioActivity)( - finishedAt = Some(Timestamp.from(activity.dateFinished)), + finishedAt = activity.dateFinished.map(Timestamp.from), errorMessage = activity.errorMessage, ) case activity: ScenarioActivity.AutomaticUpdate => @@ -757,16 +757,15 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef, clock: C .map((entity.id, _)) case ScenarioActivityType.ScenarioNameChanged => (for { - oldName <- additionalPropertyFromEntity(entity, "oldName") - newName <- additionalPropertyFromEntity(entity, "newName") + oldNameAndNewName <- extractOldNameAndNewNameForRename(entity) } yield ScenarioActivity.ScenarioNameChanged( scenarioId = scenarioIdFromEntity(entity), scenarioActivityId = entity.activityId, user = userFromEntity(entity), date = entity.createdAt.toInstant, scenarioVersion = entity.scenarioVersion, - oldName = oldName, - newName = newName + oldName = oldNameAndNewName._1, + newName = oldNameAndNewName._2 )).map((entity.id, _)) case ScenarioActivityType.CommentAdded => (for { @@ -836,29 +835,31 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef, clock: C destinationEnvironment = Environment(destinationEnvironment), )).map((entity.id, _)) case ScenarioActivityType.PerformedSingleExecution => - (for { - finishedAt <- entity.finishedAt.map(_.toInstant).toRight("Missing finishedAt") - } yield ScenarioActivity.PerformedSingleExecution( - scenarioId = scenarioIdFromEntity(entity), - scenarioActivityId = entity.activityId, - user = userFromEntity(entity), - date = entity.createdAt.toInstant, - scenarioVersion = entity.scenarioVersion, - dateFinished = finishedAt, - errorMessage = entity.errorMessage, - )).map((entity.id, _)) + ScenarioActivity + .PerformedSingleExecution( + 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.PerformedScheduledExecution => - (for { - finishedAt <- entity.finishedAt.map(_.toInstant).toRight("Missing finishedAt") - } yield ScenarioActivity.PerformedScheduledExecution( - scenarioId = scenarioIdFromEntity(entity), - scenarioActivityId = entity.activityId, - user = userFromEntity(entity), - date = entity.createdAt.toInstant, - scenarioVersion = entity.scenarioVersion, - dateFinished = finishedAt, - errorMessage = entity.errorMessage, - )).map((entity.id, _)) + 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") @@ -875,7 +876,9 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef, clock: C )).map((entity.id, _)) case ScenarioActivityType.CustomAction(actionName) => - ScenarioActivity + (for { + comment <- commentFromEntity(entity) + } yield ScenarioActivity .CustomAction( scenarioId = scenarioIdFromEntity(entity), scenarioActivityId = entity.activityId, @@ -883,9 +886,35 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef, clock: C date = entity.createdAt.toInstant, scenarioVersion = entity.scenarioVersion, actionName = actionName, - ) - .asRight - .map((entity.id, _)) + 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") } } diff --git a/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala b/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala index 3c99aef31c5..86bb74deef7 100644 --- a/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala +++ b/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala @@ -8,10 +8,12 @@ import db.migration.V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinit import io.circe.syntax.EncoderOps import org.scalatest.freespec.AnyFreeSpecLike import org.scalatest.matchers.should.Matchers -import pl.touk.nussknacker.engine.api.deployment.ScenarioActionName +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 @@ -19,6 +21,7 @@ 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 @@ -38,109 +41,46 @@ class V1_056__MigrateActionsAndCommentsToScenarioActivities override protected val profile: JdbcProfile = HsqldbProfile - "When data is present in old actions and comments tables" - { - "migrate data to scenario_activities table" in { - import HsqldbProfile.api._ - - val runner = newDBIOActionRunner(testDbRef) - - val migration = new Migration(HsqldbProfile) - val processActionsDefinitions = new ProcessActionsDefinitions(profile) - val commentsDefinitions = new CommentsDefinitions(profile) - val activitiesDefinitions = new ScenarioActivitiesDefinitions(profile) - - val now: Timestamp = Timestamp.from(Instant.now) - val user = "John Doe" - val versionId = VersionId(5L) - - val processInsertQuery = processesTable returning - processesTable.map(_.id) into ((item, id) => item.copy(id = id)) - val commentInsertQuery = commentsDefinitions.table returning - commentsDefinitions.table.map(_.id) into ((item, id) => item.copy(id = id)) - val actionInsertQuery = processActionsDefinitions.table returning - processActionsDefinitions.table.map(_.id) into ((item, id) => item.copy(id = id)) - - def processEntity() = ProcessEntityData( - id = ProcessId(-1L), - name = ProcessName("2024_Q3_6917_NETFLIX"), - processCategory = "test-category", - description = None, - processingType = "BatchPeriodic", - isFragment = false, - isArchived = false, - createdAt = now, - createdBy = user, - impersonatedByIdentity = None, - impersonatedByUsername = None - ) + import profile.api._ - def processVersionEntity(processEntity: ProcessEntityData) = ProcessVersionEntityData( - id = versionId, - 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 val runner = newDBIOActionRunner(testDbRef) - def commentEntity(processEntity: ProcessEntityData, commentId: Long) = CommentEntityData( - id = commentId, - processId = processEntity.id.value, - processVersionId = versionId.value, - content = s"Very important change $commentId", - user = user, - impersonatedByIdentity = None, - impersonatedByUsername = None, - createDate = now, - ) + private val migration = new Migration(HsqldbProfile) + private val processActionsDefinitions = new ProcessActionsDefinitions(profile) + private val commentsDefinitions = new CommentsDefinitions(profile) + private val activitiesDefinitions = new ScenarioActivitiesDefinitions(profile) - def processActionEntity(processEntity: ProcessEntityData, commentId: Long) = ProcessActionEntityData( - id = UUID.randomUUID(), - processId = processEntity.id.value, - processVersionId = Some(versionId.value), - user = user, - impersonatedByIdentity = None, - impersonatedByUsername = None, - createdAt = now, - performedAt = None, - actionName = ScenarioActionName.Deploy.value, - state = "IN_PROGRESS", - failureMessage = None, - commentId = Some(commentId), - buildInfo = None - ) + 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) - val (createdProcess, migratedCount, actionsBeingMigrated, activitiesAfterMigration) = Await.result( - runner.run( - for { - process <- processInsertQuery += processEntity() - _ <- processVersionsTable += processVersionEntity(process) - comments <- commentInsertQuery ++= List.range(1L, 100001L).map(idx => commentEntity(process, idx)) - actions <- actionInsertQuery ++= comments.map(comment => processActionEntity(process, comment.id)) - migratedCount <- migration.migrateActions - activities <- activitiesDefinitions.scenarioActivitiesTable.result - } yield (process, migratedCount, actions, activities) - ), - Duration.Inf + 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 - migratedCount shouldBe 100000 activitiesAfterMigration.length shouldBe 100000 val headActivity = @@ -162,7 +102,7 @@ class V1_056__MigrateActionsAndCommentsToScenarioActivities lastModifiedByUserName = Some(user), lastModifiedAt = Some(now), createdAt = now, - scenarioVersion = Some(versionId.value), + scenarioVersion = Some(processVersionId), comment = Some(s"Very important change $expectedOldCommentIdForHeadActivity"), attachmentId = None, finishedAt = None, @@ -171,8 +111,252 @@ class V1_056__MigrateActionsAndCommentsToScenarioActivities 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 = None, + expectedActivity = (sid, sad, user, date, sv) => + ScenarioActivity.PerformedSingleExecution( + scenarioId = sid, + scenarioActivityId = sad, + user = user, + date = date, + scenarioVersion = sv, + dateFinished = None, + errorMessage = None + ) + ) + } + "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("2024_Q3_6917_NETFLIX"), + 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/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ScenarioActivity.scala similarity index 98% rename from extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala rename to extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ScenarioActivity.scala index b78b6e3ba15..c6c91ccb505 100644 --- a/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessActivity.scala +++ b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ScenarioActivity.scala @@ -207,7 +207,7 @@ object ScenarioActivity { user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], - dateFinished: Instant, + dateFinished: Option[Instant], errorMessage: Option[String], ) extends ScenarioActivity @@ -217,7 +217,7 @@ object ScenarioActivity { user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], - dateFinished: Instant, + dateFinished: Option[Instant], errorMessage: Option[String], ) extends ScenarioActivity @@ -241,6 +241,7 @@ object ScenarioActivity { date: Instant, scenarioVersion: Option[ScenarioVersion], actionName: String, + comment: ScenarioComment, ) extends ScenarioActivity } From 8032ac846e18bb7389bdd3843dc65a9f9d7c6a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Thu, 19 Sep 2024 14:36:57 +0200 Subject: [PATCH 38/43] Regenerate swagger --- docs-internal/api/nu-designer-openapi.yaml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs-internal/api/nu-designer-openapi.yaml b/docs-internal/api/nu-designer-openapi.yaml index c21445f64d8..90903dedb02 100644 --- a/docs-internal/api/nu-designer-openapi.yaml +++ b/docs-internal/api/nu-designer-openapi.yaml @@ -4262,6 +4262,7 @@ components: - user - date - actionName + - comment - type properties: id: @@ -4279,6 +4280,8 @@ components: format: int64 actionName: type: string + comment: + $ref: '#/components/schemas/ScenarioActivityComment' type: type: string CustomActionRequest: @@ -5512,7 +5515,6 @@ components: - id - user - date - - dateFinished - type properties: id: @@ -5529,7 +5531,9 @@ components: - 'null' format: int64 dateFinished: - type: string + type: + - string + - 'null' format: date-time errorMessage: type: @@ -5544,7 +5548,6 @@ components: - id - user - date - - dateFinished - type properties: id: @@ -5561,7 +5564,9 @@ components: - 'null' format: int64 dateFinished: - type: string + type: + - string + - 'null' format: date-time errorMessage: type: From 0259c2293cf5fc5513da6ed5baf5af65ab19b01f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Thu, 19 Sep 2024 15:06:48 +0200 Subject: [PATCH 39/43] merge fixes --- ...> V1_056__CreateScenarioActivitiesDefinition.scala} | 10 +++++----- ...onsAndCommentsToScenarioActivitiesDefinition.scala} | 8 ++++---- ...es.scala => V1_056__CreateScenarioActivities.scala} | 4 ++-- ...MigrateActionsAndCommentsToScenarioActivities.scala | 9 --------- ...MigrateActionsAndCommentsToScenarioActivities.scala | 9 +++++++++ ...es.scala => V1_056__CreateScenarioActivities.scala} | 4 ++-- ...MigrateActionsAndCommentsToScenarioActivities.scala | 9 --------- ...MigrateActionsAndCommentsToScenarioActivities.scala | 9 +++++++++ .../nussknacker/ui/api/ProcessesExportResources.scala | 8 +++++++- .../ui/server/AkkaHttpBasedRouteProvider.scala | 10 +++++++--- ...igrateActionsAndCommentsToScenarioActivities.scala} | 6 +++--- 11 files changed, 48 insertions(+), 38 deletions(-) rename designer/server/src/main/scala/db/migration/{V1_055__CreateScenarioActivitiesDefinition.scala => V1_056__CreateScenarioActivitiesDefinition.scala} (93%) rename designer/server/src/main/scala/db/migration/{V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala => V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala} (97%) rename designer/server/src/main/scala/db/migration/hsql/{V1_055__CreateScenarioActivities.scala => V1_056__CreateScenarioActivities.scala} (57%) delete mode 100644 designer/server/src/main/scala/db/migration/hsql/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala create mode 100644 designer/server/src/main/scala/db/migration/hsql/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala rename designer/server/src/main/scala/db/migration/postgres/{V1_055__CreateScenarioActivities.scala => V1_056__CreateScenarioActivities.scala} (58%) delete mode 100644 designer/server/src/main/scala/db/migration/postgres/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala create mode 100644 designer/server/src/main/scala/db/migration/postgres/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala rename designer/server/src/test/scala/db/migration/{V1_056__MigrateActionsAndCommentsToScenarioActivities.scala => V1_057__MigrateActionsAndCommentsToScenarioActivities.scala} (98%) diff --git a/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_056__CreateScenarioActivitiesDefinition.scala similarity index 93% rename from designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala rename to designer/server/src/main/scala/db/migration/V1_056__CreateScenarioActivitiesDefinition.scala index c636bba0ecf..2be4afa5d7d 100644 --- a/designer/server/src/main/scala/db/migration/V1_055__CreateScenarioActivitiesDefinition.scala +++ b/designer/server/src/main/scala/db/migration/V1_056__CreateScenarioActivitiesDefinition.scala @@ -1,7 +1,7 @@ package db.migration import com.typesafe.scalalogging.LazyLogging -import db.migration.V1_055__CreateScenarioActivitiesDefinition.ScenarioActivitiesDefinitions +import db.migration.V1_056__CreateScenarioActivitiesDefinition.ScenarioActivitiesDefinitions import pl.touk.nussknacker.ui.db.migration.SlickMigration import shapeless.syntax.std.tuple._ import slick.jdbc.JdbcProfile @@ -11,24 +11,24 @@ import java.sql.Timestamp import java.util.UUID import scala.concurrent.ExecutionContext.Implicits.global -trait V1_055__CreateScenarioActivitiesDefinition extends SlickMigration with LazyLogging { +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_055__CreateScenarioActivitiesDefinition") + 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_055__CreateScenarioActivitiesDefinition") + } yield logger.info("Execution finished for migration V1_056__CreateScenarioActivitiesDefinition") } } -object V1_055__CreateScenarioActivitiesDefinition { +object V1_056__CreateScenarioActivitiesDefinition { class ScenarioActivitiesDefinitions(val profile: JdbcProfile) { import profile.api._ diff --git a/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala similarity index 97% rename from designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala rename to designer/server/src/main/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala index 0825041792e..bdbaa614db8 100644 --- a/designer/server/src/main/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala +++ b/designer/server/src/main/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala @@ -1,8 +1,8 @@ package db.migration import com.typesafe.scalalogging.LazyLogging -import db.migration.V1_055__CreateScenarioActivitiesDefinition.ScenarioActivitiesDefinitions -import db.migration.V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition.Migration +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 @@ -13,7 +13,7 @@ import java.sql.Timestamp import java.util.UUID import scala.concurrent.ExecutionContext.Implicits.global -trait V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends SlickMigration with LazyLogging { +trait V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends SlickMigration with LazyLogging { import profile.api._ @@ -23,7 +23,7 @@ trait V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends Sl } -object V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends LazyLogging { +object V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends LazyLogging { class Migration(val profile: JdbcProfile) extends ScenarioActivityEntityFactory { diff --git a/designer/server/src/main/scala/db/migration/hsql/V1_055__CreateScenarioActivities.scala b/designer/server/src/main/scala/db/migration/hsql/V1_056__CreateScenarioActivities.scala similarity index 57% rename from designer/server/src/main/scala/db/migration/hsql/V1_055__CreateScenarioActivities.scala rename to designer/server/src/main/scala/db/migration/hsql/V1_056__CreateScenarioActivities.scala index a21fbec32d9..25b0616f091 100644 --- a/designer/server/src/main/scala/db/migration/hsql/V1_055__CreateScenarioActivities.scala +++ b/designer/server/src/main/scala/db/migration/hsql/V1_056__CreateScenarioActivities.scala @@ -1,8 +1,8 @@ package db.migration.hsql -import db.migration.V1_055__CreateScenarioActivitiesDefinition +import db.migration.V1_056__CreateScenarioActivitiesDefinition import slick.jdbc.{HsqldbProfile, JdbcProfile} -class V1_055__CreateScenarioActivities extends V1_055__CreateScenarioActivitiesDefinition { +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_056__MigrateActionsAndCommentsToScenarioActivities.scala b/designer/server/src/main/scala/db/migration/hsql/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala deleted file mode 100644 index 463adb131a3..00000000000 --- a/designer/server/src/main/scala/db/migration/hsql/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala +++ /dev/null @@ -1,9 +0,0 @@ -package db.migration.hsql - -import db.migration.V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition -import slick.jdbc.{HsqldbProfile, JdbcProfile} - -class V1_056__MigrateActionsAndCommentsToScenarioActivities - extends V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition { - 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_055__CreateScenarioActivities.scala b/designer/server/src/main/scala/db/migration/postgres/V1_056__CreateScenarioActivities.scala similarity index 58% rename from designer/server/src/main/scala/db/migration/postgres/V1_055__CreateScenarioActivities.scala rename to designer/server/src/main/scala/db/migration/postgres/V1_056__CreateScenarioActivities.scala index d466e9f87bb..00be93e0e77 100644 --- a/designer/server/src/main/scala/db/migration/postgres/V1_055__CreateScenarioActivities.scala +++ b/designer/server/src/main/scala/db/migration/postgres/V1_056__CreateScenarioActivities.scala @@ -1,8 +1,8 @@ package db.migration.postgres -import db.migration.V1_055__CreateScenarioActivitiesDefinition +import db.migration.V1_056__CreateScenarioActivitiesDefinition import slick.jdbc.{JdbcProfile, PostgresProfile} -class V1_055__CreateScenarioActivities extends V1_055__CreateScenarioActivitiesDefinition { +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_056__MigrateActionsAndCommentsToScenarioActivities.scala b/designer/server/src/main/scala/db/migration/postgres/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala deleted file mode 100644 index b49dcd45b09..00000000000 --- a/designer/server/src/main/scala/db/migration/postgres/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala +++ /dev/null @@ -1,9 +0,0 @@ -package db.migration.postgres - -import db.migration.V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition -import slick.jdbc.{JdbcProfile, PostgresProfile} - -class V1_056__MigrateActionsAndCommentsToScenarioActivities - extends V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition { - 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/ProcessesExportResources.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ProcessesExportResources.scala index 0a874c97e9f..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 @@ -9,11 +9,17 @@ 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.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.activities.ScenarioActivityRepository -import pl.touk.nussknacker.ui.process.repository.{DBIOActionRunner, FetchingProcessRepository, ScenarioWithDetailsEntity} +import pl.touk.nussknacker.ui.process.repository.{ + DBIOActionRunner, + FetchingProcessRepository, + ScenarioWithDetailsEntity +} import pl.touk.nussknacker.ui.security.api.LoggedUser import pl.touk.nussknacker.ui.uiresolving.UIProcessResolver import pl.touk.nussknacker.ui.util._ 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 7758b2899d8..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 @@ -162,8 +162,8 @@ class AkkaHttpBasedRouteProvider( implicit val implicitDbioRunner: DBIOActionRunner = dbioRunner val scenarioActivityRepository = new DbScenarioActivityRepository(dbRef, designerClock) val actionRepository = new DbScenarioActionRepository(dbRef, modelBuildInfo) - val scenarioLabelsRepository = new ScenarioLabelsRepository(dbRef) - valprocessRepository = DBFetchingProcessRepository.create(dbRef, actionRepository, scenarioLabelsRepository) + 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) @@ -251,7 +251,11 @@ class AkkaHttpBasedRouteProvider( val authenticationResources = AuthenticationResources(resolvedConfig, getClass.getClassLoader, sttpBackend) val authManager = new AuthManager(authenticationResources) - Initialization.init(migrations, dbRef, designerClock, processRepository, + Initialization.init( + migrations, + dbRef, + designerClock, + processRepository, processActivityRepository, scenarioLabelsRepository, environment diff --git a/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala b/designer/server/src/test/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala similarity index 98% rename from designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala rename to designer/server/src/test/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala index 86bb74deef7..8f21dfe840d 100644 --- a/designer/server/src/test/scala/db/migration/V1_056__MigrateActionsAndCommentsToScenarioActivities.scala +++ b/designer/server/src/test/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala @@ -1,10 +1,10 @@ package db.migration -import db.migration.V1_055__CreateScenarioActivitiesDefinition.{ +import db.migration.V1_056__CreateScenarioActivitiesDefinition.{ ScenarioActivitiesDefinitions, ScenarioActivityEntityData } -import db.migration.V1_056__MigrateActionsAndCommentsToScenarioActivitiesDefinition._ +import db.migration.V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition._ import io.circe.syntax.EncoderOps import org.scalatest.freespec.AnyFreeSpecLike import org.scalatest.matchers.should.Matchers @@ -31,7 +31,7 @@ import scala.concurrent.Await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.Duration -class V1_056__MigrateActionsAndCommentsToScenarioActivities +class V1_057__MigrateActionsAndCommentsToScenarioActivities extends AnyFreeSpecLike with Matchers with NuItTest From 818a71aacdcc8224378a141e3887793e113879e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Thu, 19 Sep 2024 15:35:44 +0200 Subject: [PATCH 40/43] add custom action migration test --- ...eActionsAndCommentsToScenarioActivities.scala | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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 index 8f21dfe840d..e8d8bbbf3de 100644 --- a/designer/server/src/test/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala +++ b/designer/server/src/test/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala @@ -217,6 +217,22 @@ class V1_057__MigrateActionsAndCommentsToScenarioActivities ) ) } + "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( From eb1d0364c0244e77aeef66ff6322d72020192203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Thu, 19 Sep 2024 16:38:26 +0200 Subject: [PATCH 41/43] Run now comment --- .../repository/activities/DbScenarioActivityRepository.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index bbc91c74fc4..d3ddac1d1b9 100644 --- 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 @@ -282,8 +282,8 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef, clock: C None case _: ScenarioActivity.OutgoingMigration => None - case _: ScenarioActivity.PerformedSingleExecution => - None + case activity: ScenarioActivity.PerformedSingleExecution => + toComment(id, activity, activity.comment, Some("Run now: ")) case _: ScenarioActivity.PerformedScheduledExecution => None case _: ScenarioActivity.AutomaticUpdate => From 47b84cf5ca0264ad34759b3615bbb1096cc3015c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Thu, 19 Sep 2024 16:59:49 +0200 Subject: [PATCH 42/43] Run now comment --- .../api/ScenarioActivityApiHttpService.scala | 2 ++ .../description/scenarioActivity/Dtos.scala | 1 + .../scenarioActivity/Examples.scala | 10 ++++++++ .../DbScenarioActivityRepository.scala | 24 +++++++++---------- ...tionsAndCommentsToScenarioActivities.scala | 5 ++-- docs-internal/api/nu-designer-openapi.yaml | 14 +++++++++++ .../api/deployment/ScenarioActivity.scala | 1 + 7 files changed, 43 insertions(+), 14 deletions(-) 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 07188e1833b..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 @@ -385,6 +385,7 @@ class ScenarioActivityApiHttpService( user, date, scenarioVersion, + comment, dateFinished, errorMessage ) => @@ -393,6 +394,7 @@ class ScenarioActivityApiHttpService( user = user.name.value, date = date, scenarioVersion = scenarioVersion.map(_.value), + comment = toDto(comment), dateFinished = dateFinished, errorMessage = errorMessage, ) 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 545c81a0b4d..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,6 +431,7 @@ object Dtos { user: String, date: Instant, scenarioVersion: Option[Long], + comment: ScenarioActivityComment, dateFinished: Option[Instant], errorMessage: Option[String], ) extends ScenarioActivity 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 f0dd198a737..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 @@ -180,6 +180,11 @@ object Examples { user = "some user", date = Instant.parse("2024-01-17T14:21:17Z"), scenarioVersion = Some(1), + 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"), ), @@ -188,6 +193,11 @@ object Examples { user = "some user", date = Instant.parse("2024-01-17T14:21:17Z"), scenarioVersion = Some(1), + 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, ), 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 index d3ddac1d1b9..7b89d3d1818 100644 --- 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 @@ -835,18 +835,18 @@ class DbScenarioActivityRepository(override protected val dbRef: DbRef, clock: C destinationEnvironment = Environment(destinationEnvironment), )).map((entity.id, _)) case ScenarioActivityType.PerformedSingleExecution => - ScenarioActivity - .PerformedSingleExecution( - 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, _)) + (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( 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 index e8d8bbbf3de..ac49cd06991 100644 --- a/designer/server/src/test/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala +++ b/designer/server/src/test/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala @@ -204,7 +204,7 @@ class V1_057__MigrateActionsAndCommentsToScenarioActivities "migrate custom action 'run now' with comment to scenario_activities table" in { testMigratingActionWithComment( scenarioActionName = InstantBatchCustomAction.name, - actionComment = None, + actionComment = Some("Run now: Deployed at the request of business"), expectedActivity = (sid, sad, user, date, sv) => ScenarioActivity.PerformedSingleExecution( scenarioId = sid, @@ -213,7 +213,8 @@ class V1_057__MigrateActionsAndCommentsToScenarioActivities date = date, scenarioVersion = sv, dateFinished = None, - errorMessage = None + errorMessage = None, + comment = Available("Deployed at the request of business", user.name, date) ) ) } diff --git a/docs-internal/api/nu-designer-openapi.yaml b/docs-internal/api/nu-designer-openapi.yaml index c573a279079..6ee2b7ee855 100644 --- a/docs-internal/api/nu-designer-openapi.yaml +++ b/docs-internal/api/nu-designer-openapi.yaml @@ -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 @@ -5741,6 +5752,7 @@ components: - id - user - date + - comment - type properties: id: @@ -5756,6 +5768,8 @@ components: - integer - 'null' format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' dateFinished: type: - string 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 index c6c91ccb505..97ad3d2255b 100644 --- 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 @@ -207,6 +207,7 @@ object ScenarioActivity { user: ScenarioUser, date: Instant, scenarioVersion: Option[ScenarioVersion], + comment: ScenarioComment, dateFinished: Option[Instant], errorMessage: Option[String], ) extends ScenarioActivity From 85217001757d6a8780d1773c5f029383f167f57f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Fri, 20 Sep 2024 11:43:57 +0200 Subject: [PATCH 43/43] review changes --- .../V1_057__MigrateActionsAndCommentsToScenarioActivities.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index ac49cd06991..4b861777356 100644 --- a/designer/server/src/test/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala +++ b/designer/server/src/test/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala @@ -301,7 +301,7 @@ class V1_057__MigrateActionsAndCommentsToScenarioActivities private def processEntity(user: String, timestamp: Timestamp) = ProcessEntityData( id = ProcessId(-1L), - name = ProcessName("2024_Q3_6917_NETFLIX"), + name = ProcessName("2023_Q1_1234_STREAMING_SERVICE"), processCategory = "test-category", description = None, processingType = "BatchPeriodic",