Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ client := loops.NewClient("YOUR_API_KEY",
- Components — `GetComponent`, `ListComponents`, `CreateComponent`, `UpdateComponent`
- Themes — `GetTheme`, `ListThemes`, `CreateTheme`, `UpdateTheme`
- Uploads — `Upload`, `CreateUpload`, `CompleteUpload`
- Workflows — `ListWorkflows`, `GetWorkflow`, `GetWorkflowNode`, `CreateWorkflow`, `UpdateWorkflow`, `ChangeWorkflowMailingList`, `CreateWorkflowNode`, `UpdateWorkflowNode`, `AddWorkflowBranch`, `DeleteWorkflowNode`, `DeleteWorkflowNodeRecursive`
- Workflows — `ListWorkflows`, `GetWorkflow`, `GetWorkflowNode`, `CreateWorkflow`, `UpdateWorkflow`, `ChangeWorkflowMailingList`, `CreateWorkflowNode`, `UpdateWorkflowNode`, `AddWorkflowBranch`, `RerouteNodeConnection`, `DeleteWorkflowNode`, `DeleteWorkflowNodeRecursive`

Full reference: [pkg.go.dev/github.com/loops-so/loops-go](https://pkg.go.dev/github.com/loops-so/loops-go).

Expand Down
152 changes: 134 additions & 18 deletions workflows.go
Original file line number Diff line number Diff line change
Expand Up @@ -1255,13 +1255,14 @@ type ChangeWorkflowMailingListRequest struct {

// ChangeWorkflowMailingListResponse is returned by
// [Client.ChangeWorkflowMailingList]. Status is one of the
// WorkflowMutationStatus* values. WorkflowRevisionID is set only when Status
// is "updated".
// WorkflowMutationStatus* values. WorkflowRevisionID and Workflow are set only
// when Status is "updated".
type ChangeWorkflowMailingListResponse struct {
Status string `json:"status"`
MailingListID *string `json:"mailingListId"`
WorkflowRevisionID *string `json:"workflowRevisionId,omitempty"`
QueuedContactCount float64 `json:"queuedContactCount"`
Status string `json:"status"`
MailingListID *string `json:"mailingListId"`
WorkflowRevisionID *string `json:"workflowRevisionId,omitempty"`
QueuedContactCount float64 `json:"queuedContactCount"`
Workflow *SimplifiedWorkflow `json:"workflow,omitempty"`
}

// ChangeWorkflowMailingList changes the mailing list of the workflow
Expand Down Expand Up @@ -1312,6 +1313,7 @@ func (c *Client) ChangeWorkflowMailingList(id string, req ChangeWorkflowMailingL
const (
WorkflowInsertModeBetween = "between"
WorkflowInsertModeBefore = "before"
WorkflowInsertModeAfter = "after"
)

// Node types that can be created via [Client.CreateWorkflowNode]. Trigger and
Expand All @@ -1329,8 +1331,11 @@ const (
// InsertMode selects the placement:
// - "between" inserts between FromNodeID and ToNodeID (FromNodeID must
// currently point to ToNodeID); both are required.
// - "before" inserts before BeforeNodeID, which must have an incoming parent
// and cannot be a trigger node.
// - "before" inserts before ToNodeID, which must have an incoming parent and
// cannot be a trigger node.
// - "after" inserts after FromNodeID, valid only when FromNodeID has exactly
// one outgoing node (not zero, not multiple, and not an exit node). Use
// "between" with an explicit ToNodeID when the source has multiple outputs.
//
// ExpectedRevisionID is required and always sent (as JSON null when nil).
// NodeTypeName is one of the CreateWorkflowNodeType* constants.
Expand All @@ -1340,7 +1345,10 @@ type CreateWorkflowNodeRequest struct {
NodeTypeName string
FromNodeID string
ToNodeID string
BeforeNodeID string

// Deprecated: the API now expects ToNodeID for "before" mode. Set ToNodeID
// instead; BeforeNodeID is only sent when ToNodeID is empty.
BeforeNodeID string
}

// CreateWorkflowNodeResponse is returned by [Client.CreateWorkflowNode].
Expand All @@ -1363,7 +1371,13 @@ func (c *Client) CreateWorkflowNode(id string, req CreateWorkflowNodeRequest) (*
body["fromNodeId"] = req.FromNodeID
body["toNodeId"] = req.ToNodeID
case WorkflowInsertModeBefore:
body["beforeNodeId"] = req.BeforeNodeID
if req.ToNodeID != "" {
body["toNodeId"] = req.ToNodeID
} else {
body["beforeNodeId"] = req.BeforeNodeID
}
case WorkflowInsertModeAfter:
body["fromNodeId"] = req.FromNodeID
}

b, err := json.Marshal(body)
Expand Down Expand Up @@ -1508,8 +1522,33 @@ type UpdateWorkflowNodeRequest struct {
}

// UpdateWorkflowNodeResponse is the updated node returned by
// [Client.UpdateWorkflowNode], with the latest workflow revision token.
type UpdateWorkflowNodeResponse = WorkflowMutationNodeWithRevision
// [Client.UpdateWorkflowNode] (its fields stay flat via the embedded type)
// plus the latest simplified workflow.
type UpdateWorkflowNodeResponse struct {
WorkflowMutationNodeWithRevision
Workflow SimplifiedWorkflow `json:"workflow"`
}

// UnmarshalJSON decodes the node and revision via the embedded type and then
// the latest workflow.
func (n *UpdateWorkflowNodeResponse) UnmarshalJSON(data []byte) error {
if err := n.WorkflowMutationNodeWithRevision.UnmarshalJSON(data); err != nil {
return err
}
var extra struct {
Workflow SimplifiedWorkflow `json:"workflow"`
}
if err := json.Unmarshal(data, &extra); err != nil {
return err
}
n.Workflow = extra.Workflow
return nil
}

// MarshalJSON encodes the embedded node and revision and adds the workflow.
func (n UpdateWorkflowNodeResponse) MarshalJSON() ([]byte, error) {
return mergeMarshal(n.WorkflowMutationNodeWithRevision, map[string]any{"workflow": n.Workflow})
}

// UpdateWorkflowNode applies req.Payload to the node identified by
// workflowID/nodeID and returns the updated node.
Expand Down Expand Up @@ -1606,13 +1645,14 @@ type DeleteWorkflowNodeRequest struct {

// DeleteWorkflowNodeResponse is returned by [Client.DeleteWorkflowNode] and
// [Client.DeleteWorkflowNodeRecursive]. Status is one of the
// WorkflowMutationStatus* values. WorkflowRevisionID is set only when Status
// is "deleted".
// WorkflowMutationStatus* values. WorkflowRevisionID and Workflow are set only
// when Status is "deleted".
type DeleteWorkflowNodeResponse struct {
Status string `json:"status"`
NodeIDs []string `json:"nodeIds"`
WorkflowRevisionID *string `json:"workflowRevisionId,omitempty"`
QueuedContactCount float64 `json:"queuedContactCount"`
Status string `json:"status"`
NodeIDs []string `json:"nodeIds"`
WorkflowRevisionID *string `json:"workflowRevisionId,omitempty"`
QueuedContactCount float64 `json:"queuedContactCount"`
Workflow *SimplifiedWorkflow `json:"workflow,omitempty"`
}

func (c *Client) deleteWorkflowNode(path string, req DeleteWorkflowNodeRequest) (*DeleteWorkflowNodeResponse, error) {
Expand Down Expand Up @@ -1667,3 +1707,79 @@ func (c *Client) DeleteWorkflowNode(workflowID, nodeID string, req DeleteWorkflo
func (c *Client) DeleteWorkflowNodeRecursive(workflowID, nodeID string, req DeleteWorkflowNodeRequest) (*DeleteWorkflowNodeResponse, error) {
return c.deleteWorkflowNode("/workflows/"+workflowID+"/nodes/"+nodeID+"/recursive", req)
}

// RerouteNodeConnectionRequest is the request body for
// [Client.RerouteNodeConnection]. ExpectedRevisionID is required and always
// sent (as JSON null when nil). NewTargetNodeID is the valid node that should
// receive the source node's outgoing connection.
type RerouteNodeConnectionRequest struct {
ExpectedRevisionID *string
NewTargetNodeID string
}

// RerouteNodeConnectionResponse is the updated source node returned by
// [Client.RerouteNodeConnection] (its fields stay flat via the embedded type)
// plus the latest simplified workflow.
type RerouteNodeConnectionResponse struct {
WorkflowMutationNodeWithRevision
Workflow SimplifiedWorkflow `json:"workflow"`
}

// UnmarshalJSON decodes the node and revision via the embedded type and then
// the latest workflow.
func (n *RerouteNodeConnectionResponse) UnmarshalJSON(data []byte) error {
if err := n.WorkflowMutationNodeWithRevision.UnmarshalJSON(data); err != nil {
return err
}
var extra struct {
Workflow SimplifiedWorkflow `json:"workflow"`
}
if err := json.Unmarshal(data, &extra); err != nil {
return err
}
n.Workflow = extra.Workflow
return nil
}

// MarshalJSON encodes the embedded node and revision and adds the workflow.
func (n RerouteNodeConnectionResponse) MarshalJSON() ([]byte, error) {
return mergeMarshal(n.WorkflowMutationNodeWithRevision, map[string]any{"workflow": n.Workflow})
}

// RerouteNodeConnection moves the single outgoing connection of the source node
// identified by workflowID/nodeID to req.NewTargetNodeID, returning the updated
// source node and the latest workflow. The source node must have exactly one
// outgoing connection; branch and experiment branch nodes cannot be rerouted.
func (c *Client) RerouteNodeConnection(workflowID, nodeID string, req RerouteNodeConnectionRequest) (*RerouteNodeConnectionResponse, error) {
body := map[string]any{
"expectedRevisionId": req.ExpectedRevisionID,
"newTargetNodeId": req.NewTargetNodeID,
}

b, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to encode request: %w", err)
}

httpReq, err := c.newRequest(http.MethodPost, "/workflows/"+workflowID+"/nodes/"+nodeID+"/reroute", bytes.NewReader(b))
if err != nil {
return nil, err
}

resp, err := c.do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, errorFromResponse(resp)
}

var result RerouteNodeConnectionResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}

return &result, nil
}
Loading