Skip to content

Commit

Permalink
Custom HTTP service SPI (#7600)
Browse files Browse the repository at this point in the history
  • Loading branch information
mgoworko authored Mar 4, 2025
1 parent 1e8cf03 commit d0a88c8
Show file tree
Hide file tree
Showing 18 changed files with 515 additions and 31 deletions.
14 changes: 14 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -1890,6 +1890,18 @@ lazy val listenerApi = (project in file("designer/listener-api"))
)
.dependsOn(extensionsApi)

lazy val customHttpServiceApi = (project in file("designer/custom-http-service-api"))
.settings(commonSettings)
.settings(
name := "nussknacker-custom-http-service-api",
libraryDependencies ++= {
Seq(
"com.typesafe.akka" %% "akka-http" % akkaHttpV,
)
}
)
.dependsOn(extensionsApi, security)

lazy val configLoaderApi = (project in file("designer/config-loader-api"))
.settings(commonSettings)
.settings(
Expand Down Expand Up @@ -2060,6 +2072,7 @@ lazy val designer = (project in file("designer/server"))
componentsApi,
restmodel,
listenerApi,
customHttpServiceApi,
configLoaderApi,
defaultHelpers % Test,
testUtils % Test,
Expand Down Expand Up @@ -2191,6 +2204,7 @@ lazy val modules = List[ProjectReference](
httpUtils,
restmodel,
listenerApi,
customHttpServiceApi,
configLoaderApi,
deploymentManagerApi,
designer,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package pl.touk.nussknacker.ui.customhttpservice

import akka.http.scaladsl.server.Route
import pl.touk.nussknacker.ui.security.api.LoggedUser

trait CustomHttpServiceProvider {
def provideRouteWithUser(implicit user: LoggedUser): Route
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package pl.touk.nussknacker.ui.customhttpservice

import cats.effect.{IO, Resource}
import com.typesafe.config.Config
import pl.touk.nussknacker.ui.customhttpservice.services.NussknackerServicesForCustomHttpService

trait CustomHttpServiceProviderFactory {

def name: String

def create(
config: Config,
services: NussknackerServicesForCustomHttpService,
): Resource[IO, CustomHttpServiceProvider]

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package pl.touk.nussknacker.ui.customhttpservice.services

final class NussknackerServicesForCustomHttpService(
val scenarioService: ScenarioService,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package pl.touk.nussknacker.ui.customhttpservice.services

import cats.effect.IO
import pl.touk.nussknacker.engine.api.component.ProcessingMode
import pl.touk.nussknacker.engine.api.deployment.{ProcessAction, ScenarioActionName, UserName}
import pl.touk.nussknacker.engine.api.process._
import pl.touk.nussknacker.engine.deployment.EngineSetupName
import pl.touk.nussknacker.ui.customhttpservice.services.ScenarioService._
import pl.touk.nussknacker.ui.security.api.LoggedUser

import java.net.URI
import java.time.Instant

trait ScenarioService {

def getLatestScenariosWithDetails(query: LatestScenariosWithDetailsQuery)(
implicit user: LoggedUser
): IO[List[ScenarioWithDetails]]

def getLatestVersionsForScenarios(query: LatestVersionsForScenariosQuery)(
implicit user: LoggedUser
): IO[Map[ProcessId, ScenarioVersionMetadata]]

}

object ScenarioService {

final case class ScenarioWithDetails(
name: ProcessName,
processId: Option[ProcessId],
processVersionId: VersionId,
isLatestVersion: Boolean,
description: Option[String],
isArchived: Boolean,
isFragment: Boolean,
processingType: ProcessingType,
processCategory: String,
processingMode: ProcessingMode,
engineSetupName: EngineSetupName,
modifiedAt: Instant,
modifiedBy: String,
createdAt: Instant,
createdBy: String,
labels: List[String],
// Actions are deprecated
lastDeployedAction: Option[ProcessAction],
lastStateAction: Option[ProcessAction],
lastAction: Option[ProcessAction],
//
modelVersion: Option[Int],
state: Option[ScenarioStatus],
)

final case class ScenarioStatus(
status: String,
visibleActions: List[ScenarioActionName],
allowedActions: List[ScenarioActionName],
actionTooltips: Map[ScenarioActionName, String],
icon: URI,
tooltip: String,
description: String,
)

final case class LatestScenariosWithDetailsQuery(
isFragment: Option[Boolean] = None,
isArchived: Option[Boolean] = None,
isDeployed: Option[Boolean] = None,
categories: Option[Seq[String]] = None,
processingTypes: Option[Seq[String]] = None,
names: Option[Seq[ProcessName]] = None,
)

final case class LatestVersionsForScenariosQuery(
isFragment: Option[Boolean] = None,
isArchived: Option[Boolean] = None,
isDeployed: Option[Boolean] = None,
categories: Option[Seq[String]] = None,
processingTypes: Option[Seq[String]] = None,
names: Option[Seq[ProcessName]] = None,
excludeVersionCreatedByUsers: Option[Seq[String]] = None,
)

final case class ScenarioVersionMetadata(
versionId: VersionId,
createdAt: Instant,
createdByUser: UserName,
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package pl.touk.nussknacker.ui.customhttpservice

import cats.effect.IO
import pl.touk.nussknacker.engine.api.process.ProcessId
import pl.touk.nussknacker.restmodel.scenariodetails.{ScenarioStatusDto, ScenarioWithDetails}
import pl.touk.nussknacker.ui.customhttpservice.services.ScenarioService
import pl.touk.nussknacker.ui.process.{ProcessService, ScenarioQuery, ScenarioVersionQuery}
import pl.touk.nussknacker.ui.process.ProcessService.GetScenarioWithDetailsOptions
import pl.touk.nussknacker.ui.process.repository.ScenarioVersionMetadata
import pl.touk.nussknacker.ui.security.api.LoggedUser

import scala.concurrent.{ExecutionContext, Future}
import scala.language.implicitConversions

class ProcessServiceBasedScenarioServiceAdapter(
processService: ProcessService
)(implicit executionContext: ExecutionContext)
extends ScenarioService {

override def getLatestScenariosWithDetails(
query: ScenarioService.LatestScenariosWithDetailsQuery
)(implicit user: LoggedUser): IO[List[ScenarioService.ScenarioWithDetails]] =
processService
.getLatestProcessesWithDetails(
toDomain(query),
GetScenarioWithDetailsOptions.withoutAdditionalFields.withFetchState
)
.map(_.map(toApi))

override def getLatestVersionsForScenarios(
query: ScenarioService.LatestVersionsForScenariosQuery,
)(implicit user: LoggedUser): IO[Map[ProcessId, ScenarioService.ScenarioVersionMetadata]] =
processService
.getLatestVersionForProcesses(toDomainScenarioQuery(query), toDomainScenarioVersionQuery(query))
.map(_.map { case (processId, metadata) => (processId, toApi(metadata)) })

private implicit def deferToIO[T](f: => Future[T]): IO[T] = IO.fromFuture(IO.delay(f))

private def toDomain(query: ScenarioService.LatestScenariosWithDetailsQuery): ScenarioQuery =
ScenarioQuery(
isFragment = query.isFragment,
isArchived = query.isArchived,
isDeployed = query.isDeployed,
categories = query.categories,
processingTypes = query.processingTypes,
names = query.names,
)

private def toDomainScenarioQuery(query: ScenarioService.LatestVersionsForScenariosQuery): ScenarioQuery =
ScenarioQuery(
isFragment = query.isFragment,
isArchived = query.isArchived,
isDeployed = query.isDeployed,
categories = query.categories,
processingTypes = query.processingTypes,
names = query.names,
)

private def toDomainScenarioVersionQuery(
query: ScenarioService.LatestVersionsForScenariosQuery
): ScenarioVersionQuery =
ScenarioVersionQuery(
excludedUserNames = query.excludeVersionCreatedByUsers
)

private def toApi(metadata: ScenarioVersionMetadata): ScenarioService.ScenarioVersionMetadata =
ScenarioService.ScenarioVersionMetadata(
versionId = metadata.versionId,
createdAt = metadata.createdAt,
createdByUser = metadata.createdByUser,
)

private def toApi(scenario: ScenarioWithDetails): ScenarioService.ScenarioWithDetails =
ScenarioService.ScenarioWithDetails(
name = scenario.name,
processId = scenario.processId,
processVersionId = scenario.processVersionId,
isLatestVersion = scenario.isLatestVersion,
description = scenario.description,
isArchived = scenario.isArchived,
isFragment = scenario.isFragment,
processingType = scenario.processingType,
processCategory = scenario.processCategory,
processingMode = scenario.processingMode,
engineSetupName = scenario.engineSetupName,
modifiedAt = scenario.modifiedAt,
modifiedBy = scenario.modifiedBy,
createdAt = scenario.createdAt,
createdBy = scenario.createdBy,
labels = scenario.labels,
lastDeployedAction = scenario.lastDeployedAction,
lastStateAction = scenario.lastStateAction,
lastAction = scenario.lastAction,
modelVersion = scenario.modelVersion,
state = scenario.state.map(toApi),
)

private def toApi(scenario: ScenarioStatusDto): ScenarioService.ScenarioStatus =
ScenarioService.ScenarioStatus(
status = scenario.status.name,
visibleActions = scenario.visibleActions,
allowedActions = scenario.allowedActions,
actionTooltips = scenario.actionTooltips,
icon = scenario.icon,
tooltip = scenario.tooltip,
description = scenario.description,
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ trait ProcessService {
implicit user: LoggedUser
): Future[List[ScenarioWithDetails]]

def getLatestVersionForProcesses(query: ScenarioQuery, scenarioVersionQuery: ScenarioVersionQuery)(
implicit user: LoggedUser
): Future[Map[ProcessId, ScenarioVersionMetadata]]

def getLatestRawProcessesWithDetails[PS: ScenarioShapeFetchStrategy](query: ScenarioQuery)(
implicit user: LoggedUser
): Future[List[ScenarioWithDetailsEntity[PS]]]
Expand Down Expand Up @@ -253,6 +257,12 @@ class DBProcessService(
)
}

override def getLatestVersionForProcesses(query: ScenarioQuery, scenarioVersionQuery: ScenarioVersionQuery)(
implicit user: LoggedUser
): Future[Map[ProcessId, ScenarioVersionMetadata]] = {
fetchingProcessRepository.fetchLatestVersionForProcesses(query, scenarioVersionQuery)
}

private abstract class FetchScenarioFun[F[_]] {
def apply[PS: ScenarioShapeFetchStrategy]: Future[F[ScenarioWithDetailsEntity[PS]]]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package pl.touk.nussknacker.ui.process

final case class ScenarioVersionQuery(
excludedUserNames: Option[Seq[String]] = None,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ package pl.touk.nussknacker.ui.process.repository

import cats.Monad
import cats.data.OptionT
import cats.implicits.toFunctorOps
import cats.instances.future._
import com.typesafe.scalalogging.LazyLogging
import db.util.DBIOActionInstances._
import pl.touk.nussknacker.engine.api.ProcessVersion
import pl.touk.nussknacker.engine.api.deployment.{ProcessAction, ProcessActionState, ScenarioActionName}
import pl.touk.nussknacker.engine.api.deployment.{ProcessAction, ProcessActionState, ScenarioActionName, UserName}
import pl.touk.nussknacker.engine.api.process._
import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess
import pl.touk.nussknacker.ui.db.DbRef
import pl.touk.nussknacker.ui.db.entity._
import pl.touk.nussknacker.ui.process.{repository, ScenarioQuery}
import pl.touk.nussknacker.ui.process.{repository, ScenarioQuery, ScenarioVersionQuery}
import pl.touk.nussknacker.ui.process.label.ScenarioLabel
import pl.touk.nussknacker.ui.process.marshall.CanonicalProcessConverter
import pl.touk.nussknacker.ui.process.repository.ProcessDBQueryRepository.ProcessNotFoundError
Expand Down Expand Up @@ -108,6 +109,31 @@ abstract class DBFetchingProcessRepository[F[_]: Monad](
)
}

override def fetchLatestVersionForProcesses(
query: ScenarioQuery,
scenarioVersionQuery: ScenarioVersionQuery,
)(
implicit loggedUser: LoggedUser,
ec: ExecutionContext
): F[Map[ProcessId, ScenarioVersionMetadata]] = {
val expr: List[Option[ProcessEntityFactory#ProcessEntity => Rep[Boolean]]] = List(
query.isFragment.map(arg => process => process.isFragment === arg),
query.isArchived.map(arg => process => process.isArchived === arg),
query.categories.map(arg => process => process.processCategory.inSet(arg)),
query.processingTypes.map(arg => process => process.processingType.inSet(arg)),
query.names.map(arg => process => process.name.inSet(arg)),
)

run(
fetchLatestVersionForProcessesExcludingUsers(
process => expr.flatten.foldLeft(true: Rep[Boolean])((x, y) => x && y(process)),
scenarioVersionQuery.excludedUserNames.map(_.toSet).getOrElse(Set.empty),
).result
).map(_.toMap.map { case (processId, (versionId, timestamp, username)) =>
processId -> ScenarioVersionMetadata(versionId, timestamp.toInstant, UserName(username))
})
}

private def fetchLatestProcessDetailsByQueryAction[PS: ScenarioShapeFetchStrategy](
query: ProcessEntityFactory#ProcessEntity => Rep[Boolean],
isDeployed: Option[Boolean]
Expand Down Expand Up @@ -270,7 +296,7 @@ abstract class DBFetchingProcessRepository[F[_]: Monad](
labels: List[ScenarioLabel],
history: Option[Seq[ScenarioVersion]]
): ScenarioWithDetailsEntity[PS] = {
repository.ScenarioWithDetailsEntity[PS](
ScenarioWithDetailsEntity[PS](
processId = process.id,
name = process.name,
processVersionId = processVersion.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package pl.touk.nussknacker.ui.process.repository
import cats.Monad
import pl.touk.nussknacker.engine.api.ProcessVersion
import pl.touk.nussknacker.engine.api.process._
import pl.touk.nussknacker.ui.process.ScenarioQuery
import pl.touk.nussknacker.ui.process.{ScenarioQuery, ScenarioVersionQuery}
import pl.touk.nussknacker.ui.security.api.LoggedUser

import scala.concurrent.ExecutionContext
Expand All @@ -28,6 +28,11 @@ abstract class FetchingProcessRepository[F[_]: Monad] extends ProcessDBQueryRepo
query: ScenarioQuery
)(implicit loggedUser: LoggedUser, ec: ExecutionContext): F[List[PS]]

def fetchLatestVersionForProcesses(
query: ScenarioQuery,
scenarioVersionQuery: ScenarioVersionQuery,
)(implicit loggedUser: LoggedUser, ec: ExecutionContext): F[Map[ProcessId, ScenarioVersionMetadata]]

def getProcessVersion(
processName: ProcessName,
versionId: VersionId
Expand Down
Loading

0 comments on commit d0a88c8

Please sign in to comment.