Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 8 additions & 0 deletions pkg/core/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import (
log "github.com/sirupsen/logrus"
)

type ComponentKind string

const (
ComponentKindAction ComponentKind = "action"
ComponentKindTrigger ComponentKind = "trigger"
ComponentKindWidget ComponentKind = "widget"
)

var ErrSecretKeyNotFound = errors.New("secret or key not found")

/*
Expand Down
10 changes: 10 additions & 0 deletions pkg/core/configurable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package core

import "github.com/superplanehq/superplane/pkg/configuration"

/*
* Configurable is a component (action, trigger, widget) that can be configured.
*/
type Configurable interface {
Configuration() []configuration.Field
}
2 changes: 1 addition & 1 deletion pkg/grpc/actions/blueprints/create_blueprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func validateOutputChannelReference(registry *registry.Registry, nodes []*compon
//
// Check if the node has the output channel referenced
//
action, err := registry.GetAction(node.Action.Name)
action, err := registry.GetAction(node.Component)
if err != nil {
return err
}
Expand Down
53 changes: 24 additions & 29 deletions pkg/grpc/actions/blueprints/serialization.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func ParseBlueprint(registry *registry.Registry, organizationID string, blueprin
}

// Find shadowed names within connected components
nodeWarnings := actions.FindShadowedNameWarnings(blueprint.Nodes, blueprint.Edges)
nodeWarnings := actions.FindShadowedNameWarnings(registry, blueprint.Nodes, blueprint.Edges)

for i, edge := range blueprint.Edges {
if edge.SourceId == "" || edge.TargetId == "" {
Expand All @@ -92,7 +92,11 @@ func ParseBlueprint(registry *registry.Registry, organizationID string, blueprin
}

// Convert proto nodes to models, adding validation errors and warnings where applicable
nodes := actions.ProtoToNodes(blueprint.Nodes)
nodes, err := actions.ProtoToNodes(registry, blueprint.Nodes)
if err != nil {
return nil, nil, err
}

for i := range nodes {
if errorMsg, hasError := nodeValidationErrors[nodes[i].ID]; hasError {
nodes[i].ErrorMessage = &errorMsg
Expand All @@ -111,38 +115,29 @@ func ParseBlueprint(registry *registry.Registry, organizationID string, blueprin
}

func validateNodeRef(registry *registry.Registry, organizationID string, node *componentpb.Node) error {
switch node.Type {
case componentpb.Node_TYPE_ACTION:
if node.Action == nil {
return fmt.Errorf("action reference is required for action ref type")
}

if node.Action.Name == "" {
return fmt.Errorf("action name is required")
}

// Check if this is an application action (contains a dot)
parts := strings.SplitN(node.Action.Name, ".", 2)
if len(parts) > 2 {
return fmt.Errorf("invalid action name: %s", node.Action.Name)
}
if node.Component == "" {
return fmt.Errorf("component name is required")
}

// For application components, validate the app installation
if len(parts) == 2 {
if err := validateIntegration(organizationID, node.Integration); err != nil {
return err
}
Comment thread
lucaspin marked this conversation as resolved.
}
// Check if this is an application action (contains a dot)
parts := strings.SplitN(node.Component, ".", 2)
if len(parts) > 2 {
return fmt.Errorf("invalid component name: %s", node.Component)
}

_, err := registry.GetAction(node.Action.Name)
if err != nil {
return fmt.Errorf("action %s not found", node.Action.Name)
// For application components, validate the app installation
if len(parts) == 2 {
if err := validateIntegration(organizationID, node.Integration); err != nil {
return err
}
}

return nil
default:
return fmt.Errorf("invalid node type")
_, err := registry.GetAction(node.Component)
if err != nil {
return err
}

return nil
}

func validateIntegration(organizationID string, ref *componentpb.IntegrationRef) error {
Expand Down
18 changes: 6 additions & 12 deletions pkg/grpc/actions/canvases/change_request_actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,12 +259,9 @@ func createCanvasWithNoopNode(ctx context.Context, t *testing.T, r *support.Reso
Spec: &pb.Canvas_Spec{
Nodes: []*componentpb.Node{
{
Id: "node-1",
Name: "Initial Name",
Type: componentpb.Node_TYPE_ACTION,
Action: &componentpb.Node_ActionRef{
Name: "noop",
},
Id: "node-1",
Name: "Initial Name",
Component: "noop",
},
},
Edges: []*componentpb.Edge{},
Expand Down Expand Up @@ -293,12 +290,9 @@ func createDraftVersion(ctx context.Context, t *testing.T, r *support.ResourceRe
Spec: &pb.Canvas_Spec{
Nodes: []*componentpb.Node{
{
Id: "node-1",
Name: nodeName,
Type: componentpb.Node_TYPE_ACTION,
Action: &componentpb.Node_ActionRef{
Name: "noop",
},
Id: "node-1",
Name: nodeName,
Component: "noop",
},
},
Edges: []*componentpb.Edge{},
Expand Down
18 changes: 6 additions & 12 deletions pkg/grpc/actions/canvases/create_canvas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,24 +172,18 @@ func TestCreateCanvasRejectsInvalidEdgeChannel(t *testing.T) {
Spec: &pb.Canvas_Spec{
Nodes: []*componentpb.Node{
{
Id: "http-1",
Name: "HTTP Request",
Type: componentpb.Node_TYPE_ACTION,
Action: &componentpb.Node_ActionRef{
Name: "http",
},
Id: "http-1",
Name: "HTTP Request",
Component: "http",
Configuration: structFromAnyMap(t, map[string]any{
"method": "GET",
"url": "https://example.com",
}),
},
{
Id: "if-1",
Name: "If",
Type: componentpb.Node_TYPE_ACTION,
Action: &componentpb.Node_ActionRef{
Name: "if",
},
Id: "if-1",
Name: "If",
Component: "if",
Configuration: structFromAnyMap(t, map[string]any{
"expression": "true",
}),
Expand Down
135 changes: 27 additions & 108 deletions pkg/grpc/actions/canvases/serialization.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ func ParseCanvas(registry *registry.Registry, orgID string, canvas *pb.Canvas) (
}

nodeIDs := make(map[string]bool)
nodeTypeByID := make(map[string]componentpb.Node_Type)
nodeTypeByID := make(map[string]core.ComponentKind)
nodeValidationErrors := make(map[string]string)

for i, node := range canvas.Spec.Nodes {
Expand All @@ -252,17 +252,26 @@ func ParseCanvas(registry *registry.Registry, orgID string, canvas *pb.Canvas) (
return nil, nil, status.Errorf(codes.InvalidArgument, "node %s: duplicate node id", node.Id)
}

nodeType, err := registry.ComponentKind(node.Component)
if err != nil {
return nil, nil, status.Errorf(codes.InvalidArgument, "node %d: %v", i, err)
}
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

nodeIDs[node.Id] = true
nodeTypeByID[node.Id] = node.Type
nodeTypeByID[node.Id] = nodeType

if err := validateNodeRef(registry, orgID, node); err != nil {
nodeValidationErrors[node.Id] = err.Error()
}
}

// Find shadowed names within connected components
nodeWarnings := actions.FindShadowedNameWarnings(canvas.Spec.Nodes, canvas.Spec.Edges)
nodes := actions.ProtoToNodes(canvas.Spec.Nodes)
nodeWarnings := actions.FindShadowedNameWarnings(registry, canvas.Spec.Nodes, canvas.Spec.Edges)
nodes, err := actions.ProtoToNodes(registry, canvas.Spec.Nodes)
if err != nil {
return nil, nil, err
}

nodesByID := make(map[string]models.Node, len(nodes))
for _, node := range nodes {
nodesByID[node.ID] = node
Expand All @@ -281,11 +290,11 @@ func ParseCanvas(registry *registry.Registry, orgID string, canvas *pb.Canvas) (
return nil, nil, status.Errorf(codes.InvalidArgument, "edge %d: target node %s not found", i, edge.TargetId)
}

if nodeTypeByID[edge.SourceId] == componentpb.Node_TYPE_WIDGET {
if nodeTypeByID[edge.SourceId] == core.ComponentKindWidget {
return nil, nil, status.Errorf(codes.InvalidArgument, "edge %d: widget nodes cannot be used as source nodes", i)
}

if nodeTypeByID[edge.TargetId] == componentpb.Node_TYPE_WIDGET {
if nodeTypeByID[edge.TargetId] == core.ComponentKindWidget {
return nil, nil, status.Errorf(codes.InvalidArgument, "edge %d: widget nodes cannot be used as target nodes", i)
}

Expand Down Expand Up @@ -325,118 +334,28 @@ func ParseCanvas(registry *registry.Registry, orgID string, canvas *pb.Canvas) (
}

func validateNodeRef(registry *registry.Registry, organizationID string, node *componentpb.Node) error {
switch node.Type {
case componentpb.Node_TYPE_ACTION:
if node.Action == nil {
return fmt.Errorf("action reference is required for action ref type")
}

if node.Action.Name == "" {
return fmt.Errorf("action name is required")
}

action, err := findAndValidateAction(registry, organizationID, node)
if err != nil {
return err
}

return configuration.ValidateConfiguration(action.Configuration(), node.Configuration.AsMap())

case componentpb.Node_TYPE_BLUEPRINT:
if node.Blueprint == nil {
return fmt.Errorf("blueprint reference is required for blueprint ref type")
}

if node.Blueprint.Id == "" {
return fmt.Errorf("blueprint ID is required")
}

blueprint, err := models.FindBlueprint(organizationID, node.Blueprint.Id)
if err != nil {
return fmt.Errorf("blueprint %s not found", node.Blueprint.Id)
}

return configuration.ValidateConfiguration(blueprint.Configuration, node.Configuration.AsMap())

case componentpb.Node_TYPE_TRIGGER:
if node.Trigger == nil {
return fmt.Errorf("trigger reference is required for trigger ref type")
}

if node.Trigger.Name == "" {
return fmt.Errorf("trigger name is required")
}

trigger, err := findAndValidateTrigger(registry, organizationID, node)
if err != nil {
return err
}

return configuration.ValidateConfiguration(trigger.Configuration(), node.Configuration.AsMap())

case componentpb.Node_TYPE_WIDGET:
if node.Widget == nil {
return fmt.Errorf("widget reference is required for widget ref type")
}

if node.Widget.Name == "" {
return fmt.Errorf("widget name is required")
}

widget, err := findAndValidateWidget(registry, organizationID, node)
if err != nil {
return err
}

return configuration.ValidateConfiguration(widget.Configuration(), node.Configuration.AsMap())

default:
return fmt.Errorf("invalid node type: %s", node.Type)
if node.Component == "" {
return fmt.Errorf("component name is required")
}
}

func findAndValidateTrigger(registry *registry.Registry, organizationID string, node *componentpb.Node) (core.Trigger, error) {
parts := strings.SplitN(node.Trigger.Name, ".", 2)
parts := strings.SplitN(node.Component, ".", 2)
if len(parts) > 2 {
return nil, fmt.Errorf("invalid trigger name: %s", node.Trigger.Name)
return fmt.Errorf("invalid component name: %s", node.Component)
}

if len(parts) == 1 {
return registry.GetTrigger(parts[0])
}

err := validateIntegration(organizationID, node.Integration)
configurable, err := registry.FindConfigurableComponent(node.Component)
if err != nil {
return nil, err
return err
}

return registry.GetIntegrationTrigger(parts[0], node.Trigger.Name)
}

func findAndValidateWidget(registry *registry.Registry, organizationID string, node *componentpb.Node) (core.Widget, error) {
if node.Widget != nil && node.Widget.Name == "" {
return nil, fmt.Errorf("widget name is required")
}

return registry.GetWidget(node.Widget.Name)
}

func findAndValidateAction(registry *registry.Registry, organizationID string, node *componentpb.Node) (core.Action, error) {
parts := strings.SplitN(node.Action.Name, ".", 2)
if len(parts) > 2 {
return nil, fmt.Errorf("invalid action name: %s", node.Action.Name)
}

if len(parts) == 1 {
return registry.GetAction(parts[0])
}

err := validateIntegration(organizationID, node.Integration)
if err != nil {
return nil, err
if len(parts) > 1 {
err := validateIntegration(organizationID, node.Integration)
if err != nil {
return err
}
}

return registry.GetIntegrationAction(parts[0], node.Action.Name)
return configuration.ValidateConfiguration(configurable.Configuration(), node.Configuration.AsMap())
}

func validateIntegration(organizationID string, ref *componentpb.IntegrationRef) error {
Expand Down
18 changes: 6 additions & 12 deletions pkg/grpc/actions/canvases/update_canvas_version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,24 +131,18 @@ func Test__UpdateCanvasVersion(t *testing.T) {
Spec: &pb.Canvas_Spec{
Nodes: []*componentpb.Node{
{
Id: "http-1",
Name: "HTTP Request",
Type: componentpb.Node_TYPE_ACTION,
Action: &componentpb.Node_ActionRef{
Name: "http",
},
Id: "http-1",
Name: "HTTP Request",
Component: "http",
Configuration: structFromAnyMap(t, map[string]any{
"method": "GET",
"url": "https://example.com",
}),
},
{
Id: "if-1",
Name: "If",
Type: componentpb.Node_TYPE_ACTION,
Action: &componentpb.Node_ActionRef{
Name: "if",
},
Id: "if-1",
Name: "If",
Component: "if",
Configuration: structFromAnyMap(t, map[string]any{
"expression": "true",
}),
Expand Down
Loading
Loading