Skip to content

Commit f6000c8

Browse files
committed
Generalize workflow system.
1 parent a369469 commit f6000c8

File tree

18 files changed

+837
-121
lines changed

18 files changed

+837
-121
lines changed

api/handle_workflows.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package api
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/checkmarble/marble-backend/dto"
7+
"github.com/checkmarble/marble-backend/pure_utils"
8+
"github.com/checkmarble/marble-backend/usecases"
9+
"github.com/gin-gonic/gin"
10+
)
11+
12+
func handleListWorkflowsForScenario(uc usecases.Usecases) func(c *gin.Context) {
13+
return func(c *gin.Context) {
14+
ctx := c.Request.Context()
15+
scenarioId := c.Param("scenarioId")
16+
17+
uc := usecasesWithCreds(ctx, uc)
18+
scenarioUsecase := uc.NewScenarioUsecase()
19+
20+
rules, err := scenarioUsecase.ListWorkflowsForScenario(ctx, scenarioId)
21+
if presentError(ctx, c, err) {
22+
return
23+
}
24+
25+
c.JSON(http.StatusOK, pure_utils.Map(rules, dto.AdaptWorkflow))
26+
}
27+
}

api/routes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,4 +287,6 @@ func addRoutes(r *gin.Engine, conf Configuration, uc usecases.Usecases, auth uti
287287
router.DELETE("/webhooks/:webhook_id", tom, handleDeleteWebhook(uc))
288288

289289
router.GET("/rule-snoozes/:rule_snooze_id", tom, handleGetSnoozesById(uc))
290+
291+
router.GET("/workflows/:scenarioId", tom, handleListWorkflowsForScenario(uc))
290292
}

dto/workflows.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package dto
2+
3+
import (
4+
"encoding/json"
5+
6+
"github.com/checkmarble/marble-backend/models"
7+
"github.com/checkmarble/marble-backend/pure_utils"
8+
)
9+
10+
type WorkflowRuleDto struct {
11+
Id string `json:"id"`
12+
Name string `json:"name"`
13+
}
14+
15+
type WorkflowConditionDto struct {
16+
Id string `json:"id"`
17+
Function string `json:"function"`
18+
Params json.Marshaler `json:"params"`
19+
}
20+
21+
type WorkflowActionDto struct {
22+
Id string `json:"id"`
23+
Action string `json:"action"`
24+
Params json.Marshaler `json:"params"`
25+
}
26+
27+
type WorkflowDto struct {
28+
WorkflowRuleDto
29+
30+
Conditions []WorkflowConditionDto `json:"conditions"`
31+
Actions []WorkflowActionDto `json:"actions"`
32+
}
33+
34+
func AdaptWorkflow(m models.Workflow) WorkflowDto {
35+
return WorkflowDto{
36+
WorkflowRuleDto: AdaptWorkflowRule(m.WorkflowRule),
37+
Conditions: pure_utils.Map(m.Conditions, AdaptWorkflowCondition),
38+
Actions: pure_utils.Map(m.Actions, AdaptWorkflowAction),
39+
}
40+
}
41+
42+
func AdaptWorkflowRule(m models.WorkflowRule) WorkflowRuleDto {
43+
return WorkflowRuleDto{
44+
Id: m.Id,
45+
Name: m.Name,
46+
}
47+
}
48+
49+
func AdaptWorkflowCondition(m models.WorkflowCondition) WorkflowConditionDto {
50+
return WorkflowConditionDto{
51+
Id: m.Id,
52+
Function: m.Function,
53+
Params: m.Params,
54+
}
55+
}
56+
57+
func AdaptWorkflowAction(m models.WorkflowAction) WorkflowActionDto {
58+
return WorkflowActionDto{
59+
Id: m.Id,
60+
Action: string(m.Action),
61+
Params: m.Params,
62+
}
63+
}

integration_test/scenario_flow_test.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515

1616
"github.com/checkmarble/marble-backend/models"
1717
"github.com/checkmarble/marble-backend/models/ast"
18-
"github.com/checkmarble/marble-backend/pure_utils"
1918
"github.com/checkmarble/marble-backend/usecases"
2019
"github.com/checkmarble/marble-backend/usecases/payload_parser"
2120
"github.com/checkmarble/marble-backend/usecases/scenarios"
@@ -407,15 +406,30 @@ func setupScenarioAndPublish(
407406
}
408407
fmt.Printf("Updated scenario iteration %+v\n", scenarioIteration)
409408

410-
workflowType := models.WorkflowAddToCaseIfPossible
411-
_, err = scenarioUsecase.UpdateScenario(ctx, models.UpdateScenarioInput{
412-
Id: scenarioId,
413-
DecisionToCaseOutcomes: []models.Outcome{models.Decline, models.Review},
414-
DecisionToCaseInboxId: pure_utils.NullFrom(inboxId),
415-
DecisionToCaseWorkflowType: &workflowType,
409+
rule, err := scenarioUsecase.CreateWorkflowRule(ctx, organizationId, models.WorkflowRule{
410+
ScenarioId: scenarioId,
411+
Name: "First rule",
416412
})
417413
if err != nil {
418-
assert.FailNow(t, "Failed to create workflow on scenario", err)
414+
assert.FailNow(t, "Could not create workflow rule", err)
415+
}
416+
417+
_, err = scenarioUsecase.CreateWorkflowCondition(ctx, organizationId, models.WorkflowCondition{
418+
RuleId: rule.Id,
419+
Function: "if_outcome_in",
420+
Params: []byte(`["decline", "review"]`),
421+
})
422+
if err != nil {
423+
assert.FailNow(t, "Could not create workflow condition", err)
424+
}
425+
426+
_, err = scenarioUsecase.CreateWorkflowAction(ctx, organizationId, models.WorkflowAction{
427+
RuleId: rule.Id,
428+
Action: models.WorkflowAddToCaseIfPossible,
429+
Params: fmt.Appendf(nil, `{"inbox_id": "%s"}`, inboxId),
430+
})
431+
if err != nil {
432+
assert.FailNow(t, "Could not create workflow action", err)
419433
}
420434

421435
return scenarioId, scenarioIterationId

models/scenarios.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ var ValidWorkflowTypes = []WorkflowType{
2222
WorkflowAddToCaseIfPossible,
2323
}
2424

25+
func WorkflowTypeFromString(s string) WorkflowType {
26+
switch s {
27+
case "ADD_TO_CASE_IF_POSSIBLE":
28+
return WorkflowAddToCaseIfPossible
29+
case "CREATE_CASE":
30+
return WorkflowCreateCase
31+
default:
32+
return WorkflowDisabled
33+
}
34+
}
35+
2536
type Scenario struct {
2637
Id string
2738
CreatedAt time.Time

models/workflows.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package models
2+
3+
import (
4+
"encoding/json"
5+
"time"
6+
7+
"github.com/checkmarble/marble-backend/models/ast"
8+
"github.com/cockroachdb/errors"
9+
"github.com/google/uuid"
10+
)
11+
12+
type Workflow struct {
13+
WorkflowRule
14+
15+
Conditions []WorkflowCondition
16+
Actions []WorkflowAction
17+
}
18+
19+
type WorkflowRule struct {
20+
Id string
21+
ScenarioId string
22+
Name string
23+
Priority int
24+
25+
CreatedAt time.Time
26+
UpdatedAt *time.Time
27+
}
28+
29+
type WorkflowCondition struct {
30+
Id string
31+
RuleId string
32+
Function string
33+
Params json.RawMessage
34+
35+
CreatedAt time.Time
36+
UpdatedAt *time.Time
37+
}
38+
39+
type WorkflowAction struct {
40+
Id string
41+
RuleId string
42+
Action WorkflowType
43+
Params json.RawMessage
44+
45+
CreatedAt time.Time
46+
UpdatedAt *time.Time
47+
}
48+
49+
func ParseWorkflowAction[T any](action WorkflowAction) (WorkflowActionSpec[T], error) {
50+
out := WorkflowActionSpec[T]{Action: action.Action}
51+
52+
switch action.Action {
53+
case WorkflowCreateCase, WorkflowAddToCaseIfPossible:
54+
if err := json.Unmarshal(action.Params, &out.Params); err != nil {
55+
return out, errors.Wrap(err, "could not unmarshal workflow action parameters")
56+
}
57+
58+
return out, nil
59+
default:
60+
return WorkflowActionSpec[T]{Action: WorkflowDisabled}, nil
61+
}
62+
}
63+
64+
type WorkflowActionSpec[T any] struct {
65+
Action WorkflowType
66+
Params T
67+
}
68+
69+
type WorkflowCaseParams struct {
70+
InboxId *uuid.UUID `json:"inbox_id"`
71+
TitleTemplate *ast.Node `json:"title_template"`
72+
}
73+
74+
type WorkflowExecution struct {
75+
AddedToCase bool
76+
WebhookIds []string
77+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package dbmodels
2+
3+
import (
4+
"encoding/json"
5+
"time"
6+
7+
"github.com/checkmarble/marble-backend/models"
8+
"github.com/checkmarble/marble-backend/pure_utils"
9+
"github.com/checkmarble/marble-backend/utils"
10+
)
11+
12+
type DbWorkflowRule struct {
13+
Id string `db:"id"`
14+
ScenarioId string `db:"scenario_id"`
15+
Name string `db:"name"`
16+
Priority int `db:"priority"`
17+
18+
CreatedAt time.Time `db:"created_at"`
19+
UpdatedAt *time.Time `db:"updated_at"`
20+
}
21+
22+
type DbWorkflowRuleWithConditions struct {
23+
DbWorkflowRule
24+
25+
Conditions []DbWorkflowCondition `db:"conditions"`
26+
Actions []DbWorkflowAction `db:"actions"`
27+
}
28+
29+
type DbWorkflowCondition struct {
30+
Id string `db:"id"`
31+
RuleId string `db:"rule_id"`
32+
Function string `db:"function"`
33+
Params json.RawMessage `db:"params"`
34+
35+
CreatedAt time.Time `db:"created_at"`
36+
UpdatedAt *time.Time `db:"updated_at"`
37+
}
38+
39+
type DbWorkflowAction struct {
40+
Id string `db:"id"`
41+
RuleId string `db:"rule_id"`
42+
Action string `db:"action"`
43+
Params json.RawMessage `db:"params"`
44+
45+
CreatedAt time.Time `db:"created_at"`
46+
UpdatedAt *time.Time `db:"updated_at"`
47+
}
48+
49+
const TABLE_WORKFLOW_RULES = "scenario_workflow_rules"
50+
const TABLE_WORKFLOW_CONDITIONS = "scenario_workflow_conditions"
51+
const TABLE_WORKFLOW_ACTIONS = "scenario_workflow_actions"
52+
53+
var WorkflowRuleColumns = utils.ColumnList[DbWorkflowRule]()
54+
var WorkflowConditionColumns = utils.ColumnList[DbWorkflowCondition]()
55+
var WorkflowActionColumns = utils.ColumnList[DbWorkflowAction]()
56+
57+
func AdaptWorkflowRule(db DbWorkflowRule) (models.WorkflowRule, error) {
58+
return models.WorkflowRule{
59+
Id: db.Id,
60+
ScenarioId: db.ScenarioId,
61+
Name: db.Name,
62+
Priority: db.Priority,
63+
CreatedAt: db.CreatedAt,
64+
UpdatedAt: db.UpdatedAt,
65+
}, nil
66+
}
67+
68+
func AdaptWorkflowCondition(db DbWorkflowCondition) (models.WorkflowCondition, error) {
69+
return models.WorkflowCondition{
70+
Id: db.Id,
71+
RuleId: db.RuleId,
72+
Function: db.Function,
73+
Params: db.Params,
74+
CreatedAt: db.CreatedAt,
75+
UpdatedAt: db.UpdatedAt,
76+
}, nil
77+
}
78+
79+
func AdaptWorkflowAction(db DbWorkflowAction) (models.WorkflowAction, error) {
80+
return models.WorkflowAction{
81+
Id: db.Id,
82+
RuleId: db.RuleId,
83+
Action: models.WorkflowTypeFromString(db.Action),
84+
Params: db.Params,
85+
CreatedAt: db.CreatedAt,
86+
UpdatedAt: db.UpdatedAt,
87+
}, nil
88+
}
89+
90+
func AdaptWorkflowRuleWithConditions(db DbWorkflowRuleWithConditions) (models.Workflow, error) {
91+
rule, err := AdaptWorkflowRule(db.DbWorkflowRule)
92+
if err != nil {
93+
return models.Workflow{}, err
94+
}
95+
96+
conditions, err := pure_utils.MapErr(db.Conditions, AdaptWorkflowCondition)
97+
if err != nil {
98+
return models.Workflow{}, err
99+
}
100+
101+
actions, err := pure_utils.MapErr(db.Actions, AdaptWorkflowAction)
102+
if err != nil {
103+
return models.Workflow{}, err
104+
}
105+
106+
return models.Workflow{
107+
WorkflowRule: rule,
108+
Conditions: conditions,
109+
Actions: actions,
110+
}, nil
111+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
-- +goose Up
2+
3+
create table scenario_workflow_rules (
4+
id uuid primary key default gen_random_uuid(),
5+
scenario_id uuid not null,
6+
name text not null,
7+
priority int,
8+
created_at timestamp with time zone default now(),
9+
updated_at timestamp with time zone,
10+
11+
constraint fk_scenario
12+
foreign key (scenario_id) references scenarios (id)
13+
on delete cascade
14+
);
15+
16+
create table scenario_workflow_conditions (
17+
id uuid primary key default gen_random_uuid(),
18+
rule_id uuid not null,
19+
function text not null,
20+
params jsonb,
21+
created_at timestamp with time zone default now(),
22+
updated_at timestamp with time zone,
23+
24+
constraint fk_rule
25+
foreign key (rule_id) references scenario_workflow_rules (id)
26+
on delete cascade
27+
);
28+
29+
create table scenario_workflow_actions (
30+
id uuid primary key default gen_random_uuid(),
31+
rule_id uuid not null,
32+
action text not null,
33+
params jsonb,
34+
created_at timestamp with time zone default now(),
35+
updated_at timestamp with time zone,
36+
37+
constraint fk_rule
38+
foreign key (rule_id) references scenario_workflow_rules (id)
39+
on delete cascade
40+
);
41+
42+
-- +goose Down
43+
44+
drop table scenario_workflow_conditions;
45+
drop table scenario_workflow_rules;

0 commit comments

Comments
 (0)