diff --git a/cmd/campaigns.go b/cmd/campaigns.go
index 4d72d67..c13af82 100644
--- a/cmd/campaigns.go
+++ b/cmd/campaigns.go
@@ -2,29 +2,23 @@ package cmd
import (
"fmt"
- "strings"
"github.com/loops-so/cli/internal/api"
"github.com/loops-so/cli/internal/config"
"github.com/spf13/cobra"
)
-func fromEmailUsername(s string) string {
- before, _, _ := strings.Cut(s, "@")
- return before
-}
-
func runCampaignsGet(cfg *config.Config, id string) (*api.Campaign, error) {
return newAPIClient(cfg).GetCampaign(id)
}
-func runCampaignsList(cfg *config.Config, params api.PaginationParams) ([]api.Campaign, error) {
+func runCampaignsList(cfg *config.Config, params api.PaginationParams) ([]api.CampaignListItem, error) {
client := newAPIClient(cfg)
if params.Cursor != "" {
campaigns, _, err := client.ListCampaigns(params)
return campaigns, err
}
- return api.Paginate(func(cursor string) ([]api.Campaign, *api.Pagination, error) {
+ return api.Paginate(func(cursor string) ([]api.CampaignListItem, *api.Pagination, error) {
return client.ListCampaigns(api.PaginationParams{
PerPage: params.PerPage,
Cursor: cursor,
@@ -53,7 +47,7 @@ var campaignsListCmd = &cobra.Command{
if isJSONOutput() {
if campaigns == nil {
- campaigns = []api.Campaign{}
+ campaigns = []api.CampaignListItem{}
}
return printJSON(cmd.OutOrStdout(), campaigns)
}
@@ -87,29 +81,13 @@ var campaignsCreateCmd = &cobra.Command{
Short: "Create a draft campaign",
RunE: func(cmd *cobra.Command, args []string) error {
name, _ := cmd.Flags().GetString("name")
- params, err := emailMessageFieldParamsFromCmd(cmd)
- if err != nil {
- return err
- }
-
- req := api.CreateCampaignRequest{Name: name}
- if len(params.Set) > 0 {
- req.EmailMessage = &api.EmailMessageFields{
- Subject: params.Subject,
- PreviewText: params.PreviewText,
- FromName: params.FromName,
- FromEmail: params.FromEmail,
- ReplyToEmail: params.ReplyToEmail,
- LMX: params.LMX,
- }
- }
cfg, err := loadConfig()
if err != nil {
return err
}
- resp, err := runCampaignsCreate(cfg, req)
+ resp, err := runCampaignsCreate(cfg, api.CreateCampaignRequest{Name: name})
if err != nil {
return err
}
@@ -118,24 +96,7 @@ var campaignsCreateCmd = &cobra.Command{
return printJSON(cmd.OutOrStdout(), resp)
}
- emailMessageID := ""
- if resp.EmailMessage != nil {
- emailMessageID = resp.EmailMessage.EmailMessageID
- }
- fmt.Fprintf(cmd.OutOrStdout(), "Created. (id: %s, emailMessageId: %s)\n", resp.CampaignID, emailMessageID)
-
- if len(resp.Warnings) > 0 {
- fmt.Fprintln(cmd.OutOrStdout())
- fmt.Fprintln(cmd.OutOrStdout(), "Warnings:")
- for _, warn := range resp.Warnings {
- if warn.Path != "" {
- fmt.Fprintf(cmd.OutOrStdout(), " [%s] %s (%s)\n", warn.Rule, warn.Message, warn.Path)
- } else {
- fmt.Fprintf(cmd.OutOrStdout(), " [%s] %s\n", warn.Rule, warn.Message)
- }
- }
- }
-
+ fmt.Fprintf(cmd.OutOrStdout(), "Created. (id: %s, emailMessageId: %s, contentRevisionId: %s)\n", resp.CampaignID, deref(resp.EmailMessageID), deref(resp.EmailMessageContentRevisionID))
return nil
},
}
@@ -176,7 +137,6 @@ func init() {
campaignsCmd.AddCommand(campaignsGetCmd)
campaignsCreateCmd.Flags().StringP("name", "n", "", "Campaign name (required)")
- addEmailMessageFieldFlags(campaignsCreateCmd)
campaignsCreateCmd.MarkFlagRequired("name")
campaignsCmd.AddCommand(campaignsCreateCmd)
diff --git a/cmd/campaigns_create_test.go b/cmd/campaigns_create_test.go
index 20dc3b5..51ccd50 100644
--- a/cmd/campaigns_create_test.go
+++ b/cmd/campaigns_create_test.go
@@ -15,18 +15,8 @@ func TestRunCampaignsCreate(t *testing.T) {
"status": "Draft",
"createdAt": "2026-04-20T10:00:00Z",
"updatedAt": "2026-04-20T10:00:00Z",
- "emailMessage": {
- "emailMessageId": "em_new",
- "campaignId": "cmp_new",
- "subject": "Hello",
- "previewText": "",
- "fromName": "",
- "fromEmail": "",
- "replyToEmail": "",
- "lmx": "",
- "contentRevisionId": "rev_1",
- "updatedAt": "2026-04-20T10:00:00Z"
- }
+ "emailMessageId": "em_new",
+ "emailMessageContentRevisionId": "rev_1"
}`
t.Run("returns response on success", func(t *testing.T) {
@@ -38,8 +28,11 @@ func TestRunCampaignsCreate(t *testing.T) {
if resp.CampaignID != "cmp_new" {
t.Errorf("CampaignID = %q, want cmp_new", resp.CampaignID)
}
- if resp.EmailMessage == nil || resp.EmailMessage.EmailMessageID != "em_new" {
- t.Errorf("EmailMessage = %v, want em_new", resp.EmailMessage)
+ if deref(resp.EmailMessageID) != "em_new" {
+ t.Errorf("EmailMessageID = %q, want em_new", deref(resp.EmailMessageID))
+ }
+ if deref(resp.EmailMessageContentRevisionID) != "rev_1" {
+ t.Errorf("EmailMessageContentRevisionID = %q, want rev_1", deref(resp.EmailMessageContentRevisionID))
}
})
@@ -51,23 +44,3 @@ func TestRunCampaignsCreate(t *testing.T) {
}
})
}
-
-func TestFromEmailUsername(t *testing.T) {
- tests := []struct {
- in string
- want string
- }{
- {"hello", "hello"},
- {"hello@acme.com", "hello"},
- {"hello@", "hello"},
- {"", ""},
- {"@acme.com", ""},
- }
- for _, tt := range tests {
- t.Run(tt.in, func(t *testing.T) {
- if got := fromEmailUsername(tt.in); got != tt.want {
- t.Errorf("fromEmailUsername(%q) = %q, want %q", tt.in, got, tt.want)
- }
- })
- }
-}
diff --git a/cmd/email_messages.go b/cmd/email_messages.go
index 02e35f2..e26832f 100644
--- a/cmd/email_messages.go
+++ b/cmd/email_messages.go
@@ -3,12 +3,18 @@ package cmd
import (
"fmt"
"os"
+ "strings"
"github.com/loops-so/cli/internal/api"
"github.com/loops-so/cli/internal/config"
"github.com/spf13/cobra"
)
+func fromEmailUsername(s string) string {
+ before, _, _ := strings.Cut(s, "@")
+ return before
+}
+
// emailMessageFieldParams holds the six content fields shared by
// `campaigns create` and `email-messages update`. Set records which fields the
// user explicitly provided (keyed by JSON field name) so partial updates can
diff --git a/cmd/email_messages_update_test.go b/cmd/email_messages_update_test.go
index a162c64..cbce4f6 100644
--- a/cmd/email_messages_update_test.go
+++ b/cmd/email_messages_update_test.go
@@ -57,6 +57,26 @@ func TestRunEmailMessagesUpdate(t *testing.T) {
})
}
+func TestFromEmailUsername(t *testing.T) {
+ tests := []struct {
+ in string
+ want string
+ }{
+ {"hello", "hello"},
+ {"hello@acme.com", "hello"},
+ {"hello@", "hello"},
+ {"", ""},
+ {"@acme.com", ""},
+ }
+ for _, tt := range tests {
+ t.Run(tt.in, func(t *testing.T) {
+ if got := fromEmailUsername(tt.in); got != tt.want {
+ t.Errorf("fromEmailUsername(%q) = %q, want %q", tt.in, got, tt.want)
+ }
+ })
+ }
+}
+
func TestEmailMessageFieldParamsFromCmd(t *testing.T) {
t.Run("unset flags are absent from Set", func(t *testing.T) {
cmd := &cobra.Command{}
diff --git a/internal/api/campaigns.go b/internal/api/campaigns.go
index 19f101d..e5e5f42 100644
--- a/internal/api/campaigns.go
+++ b/internal/api/campaigns.go
@@ -9,6 +9,15 @@ import (
)
type Campaign struct {
+ CampaignID string `json:"campaignId"`
+ EmailMessageID *string `json:"emailMessageId"`
+ Name string `json:"name"`
+ Status string `json:"status"`
+ CreatedAt string `json:"createdAt"`
+ UpdatedAt string `json:"updatedAt"`
+}
+
+type CampaignListItem struct {
CampaignID string `json:"campaignId"`
EmailMessageID *string `json:"emailMessageId"`
Name string `json:"name"`
@@ -35,18 +44,12 @@ type EmailMessageFields struct {
}
type CreateCampaignRequest struct {
- Name string `json:"name"`
- EmailMessage *EmailMessageFields `json:"emailMessage,omitempty"`
+ Name string `json:"name"`
}
type CampaignCreateResponse struct {
- CampaignID string `json:"campaignId"`
- Name string `json:"name"`
- Status string `json:"status"`
- CreatedAt string `json:"createdAt"`
- UpdatedAt string `json:"updatedAt"`
- EmailMessage *EmailMessage `json:"emailMessage,omitempty"`
- Warnings []LmxWarning `json:"warnings,omitempty"`
+ Campaign
+ EmailMessageContentRevisionID *string `json:"emailMessageContentRevisionId"`
}
func (c *Client) CreateCampaign(req CreateCampaignRequest) (*CampaignCreateResponse, error) {
@@ -102,7 +105,7 @@ func (c *Client) GetCampaign(id string) (*Campaign, error) {
return &result, nil
}
-func (c *Client) ListCampaigns(params PaginationParams) ([]Campaign, *Pagination, error) {
+func (c *Client) ListCampaigns(params PaginationParams) ([]CampaignListItem, *Pagination, error) {
q := url.Values{}
if params.PerPage != "" {
q.Set("perPage", params.PerPage)
@@ -132,8 +135,8 @@ func (c *Client) ListCampaigns(params PaginationParams) ([]Campaign, *Pagination
}
var result struct {
- Pagination Pagination `json:"pagination"`
- Data []Campaign `json:"data"`
+ Pagination Pagination `json:"pagination"`
+ Data []CampaignListItem `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, nil, fmt.Errorf("failed to decode response: %w", err)
diff --git a/internal/api/campaigns_test.go b/internal/api/campaigns_test.go
index 470885e..fd699ab 100644
--- a/internal/api/campaigns_test.go
+++ b/internal/api/campaigns_test.go
@@ -49,21 +49,8 @@ const createCampaignResponse = `{
"status": "Draft",
"createdAt": "2026-04-20T10:00:00Z",
"updatedAt": "2026-04-20T10:00:00Z",
- "emailMessage": {
- "emailMessageId": "em_new",
- "campaignId": "cmp_new",
- "subject": "Hello",
- "previewText": "Preview",
- "fromName": "Acme",
- "fromEmail": "hello",
- "replyToEmail": "support@acme.com",
- "lmx": "Hi",
- "contentRevisionId": "rev_1",
- "updatedAt": "2026-04-20T10:00:00Z"
- },
- "warnings": [
- {"rule":"unknown_attr","severity":"warning","message":" has unknown attribute \"foo\"","path":"body.0"}
- ]
+ "emailMessageId": "em_new",
+ "emailMessageContentRevisionId": "rev_1"
}`
func TestCreateCampaign(t *testing.T) {
@@ -85,12 +72,6 @@ func TestCreateCampaign(t *testing.T) {
body: `{"success":false,"message":"name is required"}`,
wantAPIErr: &APIError{StatusCode: http.StatusBadRequest, Message: "name is required"},
},
- {
- name: "lmx compile failure",
- statusCode: http.StatusUnprocessableEntity,
- body: `{"success":false,"message":"LMX failed to compile"}`,
- wantAPIErr: &APIError{StatusCode: http.StatusUnprocessableEntity, Message: "LMX failed to compile"},
- },
{
name: "invalid json",
statusCode: http.StatusCreated,
@@ -140,75 +121,36 @@ func TestCreateCampaign(t *testing.T) {
if resp.CampaignID != "cmp_new" {
t.Errorf("CampaignID = %q, want cmp_new", resp.CampaignID)
}
- if resp.EmailMessage == nil || resp.EmailMessage.EmailMessageID != "em_new" {
- t.Errorf("EmailMessage.EmailMessageID = %v, want em_new", resp.EmailMessage)
+ if resp.EmailMessageID == nil || *resp.EmailMessageID != "em_new" {
+ t.Errorf("EmailMessageID = %v, want em_new", resp.EmailMessageID)
}
- if len(resp.Warnings) != 1 || resp.Warnings[0].Rule != "unknown_attr" {
- t.Errorf("Warnings = %v, want [unknown_attr]", resp.Warnings)
+ if resp.EmailMessageContentRevisionID == nil || *resp.EmailMessageContentRevisionID != "rev_1" {
+ t.Errorf("EmailMessageContentRevisionID = %v, want rev_1", resp.EmailMessageContentRevisionID)
}
})
}
}
func TestCreateCampaign_RequestBody(t *testing.T) {
- tests := []struct {
- name string
- req CreateCampaignRequest
- wantName string
- wantEmail bool
- }{
- {
- name: "name only",
- req: CreateCampaignRequest{Name: "Spring"},
- wantName: "Spring",
- },
- {
- name: "with email message",
- req: CreateCampaignRequest{
- Name: "Spring",
- EmailMessage: &EmailMessageFields{
- Subject: "Hello",
- LMX: "Hi",
- },
- },
- wantName: "Spring",
- wantEmail: true,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- var body map[string]any
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- b, _ := io.ReadAll(r.Body)
- json.Unmarshal(b, &body)
- w.WriteHeader(http.StatusCreated)
- w.Write([]byte(createCampaignResponse))
- }))
- defer server.Close()
+ var body map[string]any
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ b, _ := io.ReadAll(r.Body)
+ json.Unmarshal(b, &body)
+ w.WriteHeader(http.StatusCreated)
+ w.Write([]byte(createCampaignResponse))
+ }))
+ defer server.Close()
- client := NewClient(server.URL, "test-key", false)
- if _, err := client.CreateCampaign(tt.req); err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
+ client := NewClient(server.URL, "test-key", false)
+ if _, err := client.CreateCampaign(CreateCampaignRequest{Name: "Spring"}); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
- if body["name"] != tt.wantName {
- t.Errorf("name = %v, want %q", body["name"], tt.wantName)
- }
- _, hasEmail := body["emailMessage"]
- if hasEmail != tt.wantEmail {
- t.Errorf("emailMessage present = %v, want %v", hasEmail, tt.wantEmail)
- }
- if tt.wantEmail {
- em, _ := body["emailMessage"].(map[string]any)
- if em["subject"] != "Hello" {
- t.Errorf("emailMessage.subject = %v, want Hello", em["subject"])
- }
- if em["lmx"] != "Hi" {
- t.Errorf("emailMessage.lmx = %v", em["lmx"])
- }
- }
- })
+ if body["name"] != "Spring" {
+ t.Errorf("name = %v, want Spring", body["name"])
+ }
+ if _, hasEmail := body["emailMessage"]; hasEmail {
+ t.Errorf("emailMessage should not be present in request body, got %v", body["emailMessage"])
}
}