Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add backend validation for Adhoc testing #6941

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions designer/client/src/actions/nk/genericAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import HttpService from "../../http/HttpService";
import { Expression, NodeValidationError, TypingResult, VariableTypes } from "../../types";

import { debounce } from "lodash";
import { TestAdhocValidationRequest } from "./testAdhoc";

export interface GenericValidationData {
validationErrors: NodeValidationError[];
Expand All @@ -20,8 +21,8 @@ export interface GenericValidationRequest {
}

export const validateGenericActionParameters = debounce(
async (processingType: string, validationRequestData: GenericValidationRequest, callback: (data: GenericValidationData) => void) => {
const { data } = await HttpService.validateGenericActionParameters(processingType, validationRequestData);
async (scenarioName: string, validationRequestData: TestAdhocValidationRequest, callback: (data: GenericValidationData) => void) => {
const { data } = await HttpService.validateAdhocTestParameters(scenarioName, validationRequestData);
callback(data);
},
500,
Expand Down
8 changes: 8 additions & 0 deletions designer/client/src/actions/nk/testAdhoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ScenarioGraph } from "../../types";
import { UIValueParameter } from "./genericAction";
import { SourceWithParametersTest } from "../../http/HttpService";

export interface TestAdhocValidationRequest {
sourceParameters: SourceWithParametersTest;
scenarioGraph: ScenarioGraph;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ const valueStartsWithQuotationMark = (value) => startsWith(value, '"') || starts
const quotationMark = (value) => (valueStartsWithQuotationMark(value) ? valueQuotationMark(value) : defaultQuotationMark);

export const stringSpelFormatter: Formatter = {
encode: (value) => quotationMark(value) + value + quotationMark(value),
encode: (value) => {
if (value === "") return value;
else return quotationMark(value) + value + quotationMark(value);
},
decode: (value) => value.substring(1, value.length - 1),
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { WindowButtonProps, WindowContentProps } from "@touk/window-manager";
import React, { ElementType, ReactElement, useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { UIParameter, VariableTypes } from "../../../types";
import { ScenarioGraph, UIParameter, VariableTypes } from "../../../types";
import { WindowContent, WindowKind } from "../../../windowManager";
import { ContentSize } from "../../graph/node-modal/node/ContentSize";
import { LoadingButtonTypes } from "../../../windowManager/LoadingButton";
Expand Down Expand Up @@ -33,8 +33,11 @@ export interface GenericActionParameters {
export interface GenericAction extends GenericActionParameters {
variableTypes: VariableTypes;
processingType: string;
scenarioName: string;
initialValues: ActionValues;
onConfirmAction: (values: ActionValues) => void;
sourceId: string;
scenarioGraph: ScenarioGraph;
}

export interface GenericActionData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,39 @@ import { useCallback, useEffect, useState } from "react";
import { ActionValues } from "./GenericActionFormContext";
import { validateGenericActionParameters } from "../../../actions/nk/genericAction";
import { GenericAction } from "./GenericActionDialog";
import { SourceWithParametersTest } from "../../../http/HttpService";

export function useGenericActionValidation(
action: Pick<GenericAction, "processingType" | "parameters" | "variableTypes">,
action: Pick<GenericAction, "scenarioName" | "parameters" | "sourceId" | "scenarioGraph">,
value: ActionValues,
): {
isValid: boolean;
errors: NodeValidationError[];
} {
const { processingType, parameters, variableTypes } = action;
const { scenarioName, parameters, sourceId, scenarioGraph } = action;
const [errors, setErrors] = useState<NodeValidationError[]>([]);

const validate = useCallback(
(value: ActionValues) => {
validateGenericActionParameters(
processingType,
scenarioName,
{
parameters: parameters.map((param) => ({
...param,
expression: value[param.name],
})),
variableTypes,
sourceParameters: {
sourceId,
parameterExpressions: parameters.reduce(
(obj, param) => ({
...obj,
[param.name]: value[param.name],
}),
{},
),
},
scenarioGraph: scenarioGraph,
},
({ validationErrors }) => setErrors(validationErrors),
);
},
[parameters, processingType, variableTypes],
[parameters, scenarioName, scenarioGraph, sourceId],
);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,12 @@ export function useTestWithFormAction(): GenericAction {
parameters,
variableTypes,
processingType,
scenarioName,
initialValues,
onConfirmAction,
sourceId,
scenarioGraph,
}),
[initialValues, onConfirmAction, parameters, processingType, variableTypes],
[initialValues, onConfirmAction, parameters, sourceId, scenarioGraph, processingType, scenarioName, variableTypes],
);
}
17 changes: 16 additions & 1 deletion designer/client/src/http/HttpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ import { AdditionalInfo } from "../components/graph/node-modal/NodeAdditionalInf
import { withoutHackOfEmptyEdges } from "../components/graph/GraphPartialsInTS/EdgeUtils";
import { CaretPosition2d, ExpressionSuggestion } from "../components/graph/node-modal/editors/expression/ExpressionSuggester";
import { GenericValidationRequest } from "../actions/nk/genericAction";
import { EventTrackingSelector } from "../containers/event-tracking";
import { EventTrackingSelectorType, EventTrackingType } from "../containers/event-tracking/use-register-tracking-events";
import { AvailableScenarioLabels, ScenarioLabelsValidationResponse } from "../components/Labels/types";
import { TestAdhocValidationRequest } from "../actions/nk/testAdhoc";

type HealthCheckProcessDeploymentType = {
status: string;
Expand Down Expand Up @@ -476,6 +476,21 @@ class HttpService {
return promise;
}

validateAdhocTestParameters(
scenarioName: string,
validationRequest: TestAdhocValidationRequest,
): Promise<AxiosResponse<ValidationData>> {
const promise = api.post(`/parameters/${encodeURIComponent(scenarioName)}/validateAdhoc`, validationRequest);
promise.catch((error) =>
this.#addError(
i18next.t("notification.error.failedToValidateAdhocTestParameters", "Failed to validate parameters"),
error,
true,
),
);
return promise;
}

validateScenarioLabels(labels: string[]): Promise<AxiosResponse<ScenarioLabelsValidationResponse>> {
const data = { labels: labels };
return api
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ import pl.touk.nussknacker.engine.api.deployment._
import pl.touk.nussknacker.engine.api.graph.ScenarioGraph
import pl.touk.nussknacker.engine.testmode.TestProcess._
import pl.touk.nussknacker.restmodel.{CustomActionRequest, CustomActionResponse}
import pl.touk.nussknacker.ui.api.description.NodesApiEndpoints.Dtos.{
TestFromParametersRequest,
prepareTestFromParametersDecoder
}
import pl.touk.nussknacker.ui.api.description.NodesApiEndpoints.Dtos.AdhocTestParametersRequest
import pl.touk.nussknacker.ui.api.utils.ScenarioDetailsOps._
import pl.touk.nussknacker.ui.api.ProcessesResources.ProcessUnmarshallingError
import pl.touk.nussknacker.ui.metrics.TimeMeasuring.measureTime
Expand Down Expand Up @@ -248,10 +245,7 @@ class ManagementResources(
path("testWithParameters" / ProcessNameSegment) { processName =>
{
(post & processDetailsForName(processName)) { process =>
val modelData = typeToConfig.forProcessingTypeUnsafe(process.processingType)
implicit val requestDecoder: Decoder[TestFromParametersRequest] =
prepareTestFromParametersDecoder(modelData)
(post & entity(as[TestFromParametersRequest])) { testParametersRequest =>
(post & entity(as[AdhocTestParametersRequest])) { testParametersRequest =>
{
canDeploy(process.idWithNameUnsafe) {
complete {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ import pl.touk.nussknacker.ui.api.description.NodesApiEndpoints.Dtos.{
import pl.touk.nussknacker.ui.api.utils.ScenarioHttpServiceExtensions
import pl.touk.nussknacker.ui.api.utils.ScenarioDetailsOps._
import pl.touk.nussknacker.ui.process.ProcessService
import pl.touk.nussknacker.ui.process.label.ScenarioLabel
import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider
import pl.touk.nussknacker.ui.process.repository.ProcessDBQueryRepository.ProcessNotFoundError
import pl.touk.nussknacker.ui.process.test.ScenarioTestService
import pl.touk.nussknacker.ui.security.api.{AuthManager, LoggedUser}
import pl.touk.nussknacker.ui.suggester.ExpressionSuggester
import pl.touk.nussknacker.ui.validation.{NodeValidator, ParametersValidator, UIProcessValidator}
Expand All @@ -52,6 +54,7 @@ class NodesApiHttpService(
processingTypeToNodeValidator: ProcessingTypeDataProvider[NodeValidator, _],
processingTypeToExpressionSuggester: ProcessingTypeDataProvider[ExpressionSuggester, _],
processingTypeToParametersValidator: ProcessingTypeDataProvider[ParametersValidator, _],
processingTypeToScenarioTestServices: ProcessingTypeDataProvider[ScenarioTestService, _],
protected override val scenarioService: ProcessService
)(override protected implicit val executionContext: ExecutionContext)
extends BaseHttpService(authManager)
Expand Down Expand Up @@ -159,6 +162,30 @@ class NodesApiHttpService(
}
}

expose {
nodesApiEndpoints.adhocTestParametersValidationEndpoint
.serverSecurityLogic(authorizeKnownUser[NodesError])
.serverLogicEitherT { implicit loggedUser =>
{ case (scenarioName, request) =>
for {
scenarioWithDetails <- getScenarioWithDetailsByName(scenarioName)
validator = processingTypeToParametersValidator.forProcessingTypeUnsafe(scenarioWithDetails.processingType)
scenarioTestService = processingTypeToScenarioTestServices.forProcessingTypeUnsafe(
scenarioWithDetails.processingType
)
inputParameters = scenarioTestService.testParametersDefinition(
request.scenarioGraph,
scenarioName,
scenarioWithDetails.isFragment,
scenarioWithDetails.labels.map(ScenarioLabel)
)
metaData = request.scenarioGraph.properties.toMetaData(scenarioName)
validationResults = validator.validate(request, inputParameters)(metaData)
} yield ParametersValidationResultDto(validationResults, validationPerformed = true)
}
}
}

expose {
nodesApiEndpoints.parametersSuggestionsEndpoint
.serverSecurityLogic(authorizeKnownUser[NodesError])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class TestInfoResources(
}
} ~ path("testParameters") {
complete {
scenarioTestService.testParametersDefinition(
scenarioTestService.testUISourceParametersDefinition(
scenarioGraph,
processName,
processDetails.isFragment,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import org.springframework.util.ClassUtils
import pl.touk.nussknacker.engine.ModelData
import pl.touk.nussknacker.engine.additionalInfo.{AdditionalInfo, MarkdownAdditionalInfo}
import pl.touk.nussknacker.engine.api.CirceUtil._
import pl.touk.nussknacker.engine.api.{LayoutData, ProcessAdditionalFields}
import pl.touk.nussknacker.engine.api.{LayoutData, ProcessAdditionalFields, StreamMetaData}
import pl.touk.nussknacker.engine.api.definition.{
FixedExpressionValue,
FixedExpressionValueWithIcon,
Expand Down Expand Up @@ -52,6 +52,7 @@ import pl.touk.nussknacker.restmodel.BaseEndpointDefinitions.SecuredEndpoint
import pl.touk.nussknacker.restmodel.definition.{UIParameter, UIValueParameter}
import pl.touk.nussknacker.restmodel.validation.ValidationResults.{NodeValidationError, NodeValidationErrorType}
import pl.touk.nussknacker.security.AuthCredentials
import pl.touk.nussknacker.ui.api.TapirCodecs.ScenarioGraphCodec._
import pl.touk.nussknacker.ui.api.TapirCodecs.ScenarioNameCodec._
import pl.touk.nussknacker.ui.api.description.NodesApiEndpoints.Dtos.NodesError.{
MalformedTypingResult,
Expand Down Expand Up @@ -348,6 +349,74 @@ class NodesApiEndpoints(auth: EndpointInput[AuthCredentials]) extends BaseEndpoi
.withSecurity(auth)
}

lazy val adhocTestParametersValidationEndpoint: SecuredEndpoint[
(ProcessName, AdhocTestParametersRequest),
NodesError,
ParametersValidationResultDto,
Any
] = {
baseNuApiEndpoint
.summary("Validate given parameters")
.tag("Nodes")
.post
.in("parameters" / path[ProcessName]("scenarioName") / "validateAdhoc")
.in(
jsonBody[AdhocTestParametersRequest]
.example(
Example.of(
summary = Some("TODO"),
value = AdhocTestParametersRequest(
TestSourceParameters("source", Map(ParameterName("name") -> Expression.spel("'Amadeus'"))),
ScenarioGraph(
ProcessProperties(StreamMetaData()),
List(),
List(),
)
)
)
)
)
.out(
statusCode(Ok).and(
jsonBody[ParametersValidationResultDto]
.examples(
List(
Example.of(
summary = Some("Validate correct parameters"),
value = ParametersValidationResultDto(
validationErrors = List.empty,
validationPerformed = true
)
),
Example.of(
summary = Some("Validate incorrect parameters"),
value = ParametersValidationResultDto(
List(
NodeValidationError(
"ExpressionParserCompilationError",
"Failed to parse expression: Bad expression type, expected: Boolean, found: Long(5)",
"There is problem with expression in field Some(condition) - it could not be parsed.",
Some("condition"),
NodeValidationErrorType.SaveAllowed,
details = None
)
),
validationPerformed = true
),
)
)
)
)
)
.errorOut(
oneOf[NodesError](
noScenarioExample,
malformedTypingResultExample
)
)
.withSecurity(auth)
}

lazy val parametersValidationEndpoint: SecuredEndpoint[
(ProcessingType, ParametersValidationRequestDto),
NodesError,
Expand Down Expand Up @@ -1400,24 +1469,22 @@ object NodesApiEndpoints {
new TypingResultDecoder(name => ClassUtils.forName(name, classLoader)).decodeTypingResults
}

def prepareTestFromParametersDecoder(modelData: ModelData): Decoder[TestFromParametersRequest] = {
implicit val parameterNameDecoder: KeyDecoder[ParameterName] = KeyDecoder.decodeKeyString.map(ParameterName.apply)
implicit val typeDecoder: Decoder[TypingResult] = prepareTypingResultDecoder(
modelData.modelClassLoader.classLoader
)
implicit val testSourceParametersDecoder: Decoder[TestSourceParameters] =
deriveConfiguredDecoder[TestSourceParameters]
deriveConfiguredDecoder[TestFromParametersRequest]
}
implicit val parameterNameCodec: KeyEncoder[ParameterName] = KeyEncoder.encodeKeyString.contramap(_.value)
implicit val parameterNameDecoder: KeyDecoder[ParameterName] = KeyDecoder.decodeKeyString.map(ParameterName.apply)

implicit val parameterNameCodec: KeyEncoder[ParameterName] = KeyEncoder.encodeKeyString.contramap(_.value)
implicit val mapParameterNameExpressionSchema: Typeclass[Map[ParameterName, Expression]] =
Schema.schemaForMap[ParameterName, Expression](_.value)
implicit val testSourceParametersDecoder: Decoder[TestSourceParameters] =
deriveConfiguredDecoder[TestSourceParameters]

@JsonCodec(encodeOnly = true) final case class TestSourceParameters(
@derive(schema, encoder, decoder)
final case class TestSourceParameters(
sourceId: String,
parameterExpressions: Map[ParameterName, Expression]
)

@JsonCodec(encodeOnly = true) final case class TestFromParametersRequest(
@derive(schema, encoder, decoder)
final case class AdhocTestParametersRequest(
sourceParameters: TestSourceParameters,
scenarioGraph: ScenarioGraph
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package pl.touk.nussknacker.ui.process.test
import com.carrotsearch.sizeof.RamUsageEstimator
import com.typesafe.scalalogging.LazyLogging
import io.circe.Json
import pl.touk.nussknacker.engine.api.definition.StringParameterEditor
import pl.touk.nussknacker.engine.api.definition.{Parameter, StringParameterEditor}
import pl.touk.nussknacker.engine.api.graph.ScenarioGraph
import pl.touk.nussknacker.engine.api.process.{ProcessIdWithName, ProcessName}
import pl.touk.nussknacker.engine.api.test.ScenarioTestData
Expand Down Expand Up @@ -50,10 +50,19 @@ class ScenarioTestService(
processName: ProcessName,
isFragment: Boolean,
labels: List[ScenarioLabel],
)(implicit user: LoggedUser): List[UISourceParameters] = {
)(implicit user: LoggedUser): Map[String, List[Parameter]] = {
val canonical = toCanonicalProcess(scenarioGraph, processName, isFragment, labels)
testInfoProvider
.getTestParameters(canonical)
}

def testUISourceParametersDefinition(
scenarioGraph: ScenarioGraph,
processName: ProcessName,
isFragment: Boolean,
labels: List[ScenarioLabel],
)(implicit user: LoggedUser): List[UISourceParameters] = {
testParametersDefinition(scenarioGraph, processName, isFragment, labels)
.map { case (id, params) => UISourceParameters(id, params.map(DefinitionsService.createUIParameter)) }
.map { assignUserFriendlyEditor }
.toList
Expand Down
Loading
Loading