From 2a76c64f886dc664b4c3ad0f34c7743e1dd73125 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Wed, 26 Nov 2025 14:56:36 -0500 Subject: [PATCH 1/3] Add `webhook list` command --- go.mod | 2 +- go.sum | 6 +- internal/cmd/root.go | 5 ++ internal/cmd/webhook/list.go | 56 +++++++++++++++ internal/cmd/webhook/list_test.go | 111 ++++++++++++++++++++++++++++++ internal/cmd/webhook/webhook.go | 63 +++++++++++++++++ internal/mock/webhook.go | 58 ++++++++++++++++ 7 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 internal/cmd/webhook/list.go create mode 100644 internal/cmd/webhook/list_test.go create mode 100644 internal/cmd/webhook/webhook.go create mode 100644 internal/mock/webhook.go diff --git a/go.mod b/go.mod index 6ab38228..f1f67b5a 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/mattn/go-shellwords v1.0.12 github.com/mitchellh/go-homedir v1.1.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c - github.com/planetscale/planetscale-go v0.145.0 + github.com/planetscale/planetscale-go v0.146.0 github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4 github.com/planetscale/psdbproxy v0.0.0-20250728082226-3f4ea3a74ec7 github.com/spf13/cobra v1.10.1 diff --git a/go.sum b/go.sum index 2e7e4c74..2fd8ec3b 100644 --- a/go.sum +++ b/go.sum @@ -175,8 +175,10 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjL github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/planetscale/noglog v0.2.1-0.20210421230640-bea75fcd2e8e h1:MZ8D+Z3m2vvqGZLvoQfpaGg/j1fNDr4j03s3PRz4rVY= github.com/planetscale/noglog v0.2.1-0.20210421230640-bea75fcd2e8e/go.mod h1:hwAsSPQdvPa3WcfKfzTXxtEq/HlqwLjQasfO6QbGo4Q= -github.com/planetscale/planetscale-go v0.145.0 h1:jdmAzU5sfdBZxVGMQXkT+BBxvOcND7cakCIQc0vdeVg= -github.com/planetscale/planetscale-go v0.145.0/go.mod h1:PheYDHAwF14wfCBak1M0J64AdPW8NUeyvgPgWqe7zpI= +github.com/planetscale/planetscale-go v0.145.1-0.20251126170422-07a76ec6411f h1:wdWyS2BnIiwJq6akI5t7vQ3TLZh0dmiKNyGp/YzfqqU= +github.com/planetscale/planetscale-go v0.145.1-0.20251126170422-07a76ec6411f/go.mod h1:PheYDHAwF14wfCBak1M0J64AdPW8NUeyvgPgWqe7zpI= +github.com/planetscale/planetscale-go v0.146.0 h1:cc65OzW4hkbhNLDApQlVAFG1680obE/OvQzEKPPsT+U= +github.com/planetscale/planetscale-go v0.146.0/go.mod h1:PheYDHAwF14wfCBak1M0J64AdPW8NUeyvgPgWqe7zpI= github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4 h1:Xv5pj20Rhfty1Tv0OVcidg4ez4PvGrpKvb6rvUwQgDs= github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4/go.mod h1:M52h5IWxAcbdQ1hSZrLAGQC4ZXslxEsK/Wh9nu3wdWs= github.com/planetscale/psdbproxy v0.0.0-20250728082226-3f4ea3a74ec7 h1:aRd6vdE1fyuSI4RVj7oCr8lFmgqXvpnPUmN85VbZCp8= diff --git a/internal/cmd/root.go b/internal/cmd/root.go index b8b64fa4..0fa9ba50 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -49,6 +49,7 @@ import ( "github.com/planetscale/cli/internal/cmd/signup" "github.com/planetscale/cli/internal/cmd/token" "github.com/planetscale/cli/internal/cmd/version" + "github.com/planetscale/cli/internal/cmd/webhook" "github.com/planetscale/cli/internal/cmdutil" "github.com/planetscale/cli/internal/config" "github.com/planetscale/cli/internal/printer" @@ -281,6 +282,10 @@ func runCmd(ctx context.Context, ver, commit, buildDate string, format *printer. databaseCmd.GroupID = "database" rootCmd.AddCommand(databaseCmd) + webhookCmd := webhook.WebhookCmd(ch) + webhookCmd.GroupID = "database" + rootCmd.AddCommand(webhookCmd) + // Vitess-specific commands connectCmd := connect.ConnectCmd(ch) connectCmd.GroupID = "vitess" diff --git a/internal/cmd/webhook/list.go b/internal/cmd/webhook/list.go new file mode 100644 index 00000000..556f3d07 --- /dev/null +++ b/internal/cmd/webhook/list.go @@ -0,0 +1,56 @@ +package webhook + +import ( + "fmt" + + "github.com/planetscale/cli/internal/cmdutil" + "github.com/planetscale/cli/internal/printer" + "github.com/planetscale/planetscale-go/planetscale" + "github.com/spf13/cobra" +) + +func ListCmd(ch *cmdutil.Helper) *cobra.Command { + cmd := &cobra.Command{ + Use: "list ", + Short: "List webhooks for a database", + Args: cmdutil.RequiredArgs("database"), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + database := args[0] + + client, err := ch.Client() + if err != nil { + return err + } + + end := ch.Printer.PrintProgress(fmt.Sprintf("Fetching webhooks for %s", printer.BoldBlue(database))) + defer end() + + webhooks, err := client.Webhooks.List(ctx, &planetscale.ListWebhooksRequest{ + Organization: ch.Config.Organization, + Database: database, + }) + if err != nil { + switch cmdutil.ErrCode(err) { + case planetscale.ErrNotFound: + return fmt.Errorf("database %s does not exist in organization %s", + printer.BoldBlue(database), printer.BoldBlue(ch.Config.Organization)) + default: + return cmdutil.HandleError(err) + } + } + + end() + + if len(webhooks) == 0 && ch.Printer.Format() == printer.Human { + ch.Printer.Printf("No webhooks exist in database %s.\n", printer.BoldBlue(database)) + return nil + } + + return ch.Printer.PrintResource(toWebhooks(webhooks)) + }, + } + + return cmd +} + diff --git a/internal/cmd/webhook/list_test.go b/internal/cmd/webhook/list_test.go new file mode 100644 index 00000000..341fc64c --- /dev/null +++ b/internal/cmd/webhook/list_test.go @@ -0,0 +1,111 @@ +package webhook + +import ( + "bytes" + "context" + "testing" + "time" + + qt "github.com/frankban/quicktest" + "github.com/planetscale/cli/internal/cmdutil" + "github.com/planetscale/cli/internal/config" + "github.com/planetscale/cli/internal/mock" + "github.com/planetscale/cli/internal/printer" + ps "github.com/planetscale/planetscale-go/planetscale" +) + +func TestWebhook_ListCmd(t *testing.T) { + c := qt.New(t) + + var buf bytes.Buffer + format := printer.JSON + p := printer.NewPrinter(&format) + p.SetResourceOutput(&buf) + + org := "planetscale" + db := "mydb" + createdAt := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC) + + webhooks := []*ps.Webhook{ + { + ID: "webhook-123", + URL: "https://example.com/webhook", + Enabled: true, + Events: []string{"branch.created", "branch.deleted"}, + CreatedAt: createdAt, + }, + } + + svc := &mock.WebhooksService{ + ListFn: func(ctx context.Context, req *ps.ListWebhooksRequest, opts ...ps.ListOption) ([]*ps.Webhook, error) { + c.Assert(req.Organization, qt.Equals, org) + c.Assert(req.Database, qt.Equals, db) + return webhooks, nil + }, + } + + ch := &cmdutil.Helper{ + Printer: p, + Config: &config.Config{ + Organization: org, + }, + Client: func() (*ps.Client, error) { + return &ps.Client{ + Webhooks: svc, + }, nil + }, + } + + cmd := ListCmd(ch) + cmd.SetArgs([]string{db}) + err := cmd.Execute() + + c.Assert(err, qt.IsNil) + c.Assert(svc.ListFnInvoked, qt.IsTrue) + + res := []*Webhook{ + {orig: webhooks[0]}, + } + c.Assert(buf.String(), qt.JSONEquals, res) +} + +func TestWebhook_ListCmd_Empty(t *testing.T) { + c := qt.New(t) + + var buf bytes.Buffer + format := printer.Human + p := printer.NewPrinter(&format) + p.SetHumanOutput(&buf) + + org := "planetscale" + db := "mydb" + + svc := &mock.WebhooksService{ + ListFn: func(ctx context.Context, req *ps.ListWebhooksRequest, opts ...ps.ListOption) ([]*ps.Webhook, error) { + c.Assert(req.Organization, qt.Equals, org) + c.Assert(req.Database, qt.Equals, db) + return []*ps.Webhook{}, nil + }, + } + + ch := &cmdutil.Helper{ + Printer: p, + Config: &config.Config{ + Organization: org, + }, + Client: func() (*ps.Client, error) { + return &ps.Client{ + Webhooks: svc, + }, nil + }, + } + + cmd := ListCmd(ch) + cmd.SetArgs([]string{db}) + err := cmd.Execute() + + c.Assert(err, qt.IsNil) + c.Assert(svc.ListFnInvoked, qt.IsTrue) + c.Assert(buf.String(), qt.Contains, "No webhooks exist") +} + diff --git a/internal/cmd/webhook/webhook.go b/internal/cmd/webhook/webhook.go new file mode 100644 index 00000000..4d379997 --- /dev/null +++ b/internal/cmd/webhook/webhook.go @@ -0,0 +1,63 @@ +package webhook + +import ( + "encoding/json" + "strings" + + "github.com/planetscale/cli/internal/cmdutil" + "github.com/planetscale/cli/internal/printer" + ps "github.com/planetscale/planetscale-go/planetscale" + "github.com/spf13/cobra" +) + +// WebhookCmd encapsulates the command for managing webhooks. +func WebhookCmd(ch *cmdutil.Helper) *cobra.Command { + cmd := &cobra.Command{ + Use: "webhook ", + Short: "List webhooks", + PersistentPreRunE: cmdutil.CheckAuthentication(ch.Config), + } + + cmd.PersistentFlags().StringVar(&ch.Config.Organization, "org", ch.Config.Organization, "The organization for the current user") + cmd.MarkPersistentFlagRequired("org") // nolint:errcheck + + cmd.AddCommand(ListCmd(ch)) + + return cmd +} + +// Webhook returns a table and json serializable webhook for printing. +type Webhook struct { + ID string `header:"id" json:"id"` + URL string `header:"url" json:"url"` + Events string `header:"events" json:"events"` + Enabled bool `header:"enabled" json:"enabled"` + CreatedAt int64 `header:"created_at,timestamp(ms|utc|human)" json:"created_at"` + + orig *ps.Webhook +} + +func (w *Webhook) MarshalJSON() ([]byte, error) { + return json.MarshalIndent(w.orig, "", " ") +} + +// toWebhook returns a struct that prints out the various fields of a webhook model. +func toWebhook(webhook *ps.Webhook) *Webhook { + return &Webhook{ + ID: webhook.ID, + URL: webhook.URL, + Events: strings.Join(webhook.Events, ", "), + Enabled: webhook.Enabled, + CreatedAt: printer.GetMilliseconds(webhook.CreatedAt), + orig: webhook, + } +} + +func toWebhooks(webhooks []*ps.Webhook) []*Webhook { + results := make([]*Webhook, 0, len(webhooks)) + for _, webhook := range webhooks { + results = append(results, toWebhook(webhook)) + } + return results +} + diff --git a/internal/mock/webhook.go b/internal/mock/webhook.go new file mode 100644 index 00000000..049261dd --- /dev/null +++ b/internal/mock/webhook.go @@ -0,0 +1,58 @@ +package mock + +import ( + "context" + + ps "github.com/planetscale/planetscale-go/planetscale" +) + +type WebhooksService struct { + ListFn func(context.Context, *ps.ListWebhooksRequest, ...ps.ListOption) ([]*ps.Webhook, error) + ListFnInvoked bool + + CreateFn func(context.Context, *ps.CreateWebhookRequest) (*ps.Webhook, error) + CreateFnInvoked bool + + GetFn func(context.Context, *ps.GetWebhookRequest) (*ps.Webhook, error) + GetFnInvoked bool + + UpdateFn func(context.Context, *ps.UpdateWebhookRequest) (*ps.Webhook, error) + UpdateFnInvoked bool + + DeleteFn func(context.Context, *ps.DeleteWebhookRequest) error + DeleteFnInvoked bool + + TestFn func(context.Context, *ps.TestWebhookRequest) error + TestFnInvoked bool +} + +func (w *WebhooksService) List(ctx context.Context, req *ps.ListWebhooksRequest, opts ...ps.ListOption) ([]*ps.Webhook, error) { + w.ListFnInvoked = true + return w.ListFn(ctx, req, opts...) +} + +func (w *WebhooksService) Create(ctx context.Context, req *ps.CreateWebhookRequest) (*ps.Webhook, error) { + w.CreateFnInvoked = true + return w.CreateFn(ctx, req) +} + +func (w *WebhooksService) Get(ctx context.Context, req *ps.GetWebhookRequest) (*ps.Webhook, error) { + w.GetFnInvoked = true + return w.GetFn(ctx, req) +} + +func (w *WebhooksService) Update(ctx context.Context, req *ps.UpdateWebhookRequest) (*ps.Webhook, error) { + w.UpdateFnInvoked = true + return w.UpdateFn(ctx, req) +} + +func (w *WebhooksService) Delete(ctx context.Context, req *ps.DeleteWebhookRequest) error { + w.DeleteFnInvoked = true + return w.DeleteFn(ctx, req) +} + +func (w *WebhooksService) Test(ctx context.Context, req *ps.TestWebhookRequest) error { + w.TestFnInvoked = true + return w.TestFn(ctx, req) +} + From 109fabba64b30e47372421b87ebccc764f7dd7d5 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Wed, 26 Nov 2025 15:01:05 -0500 Subject: [PATCH 2/3] `go mod tidy` --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index 2fd8ec3b..42a4a2fe 100644 --- a/go.sum +++ b/go.sum @@ -175,8 +175,6 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjL github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/planetscale/noglog v0.2.1-0.20210421230640-bea75fcd2e8e h1:MZ8D+Z3m2vvqGZLvoQfpaGg/j1fNDr4j03s3PRz4rVY= github.com/planetscale/noglog v0.2.1-0.20210421230640-bea75fcd2e8e/go.mod h1:hwAsSPQdvPa3WcfKfzTXxtEq/HlqwLjQasfO6QbGo4Q= -github.com/planetscale/planetscale-go v0.145.1-0.20251126170422-07a76ec6411f h1:wdWyS2BnIiwJq6akI5t7vQ3TLZh0dmiKNyGp/YzfqqU= -github.com/planetscale/planetscale-go v0.145.1-0.20251126170422-07a76ec6411f/go.mod h1:PheYDHAwF14wfCBak1M0J64AdPW8NUeyvgPgWqe7zpI= github.com/planetscale/planetscale-go v0.146.0 h1:cc65OzW4hkbhNLDApQlVAFG1680obE/OvQzEKPPsT+U= github.com/planetscale/planetscale-go v0.146.0/go.mod h1:PheYDHAwF14wfCBak1M0J64AdPW8NUeyvgPgWqe7zpI= github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4 h1:Xv5pj20Rhfty1Tv0OVcidg4ez4PvGrpKvb6rvUwQgDs= From d8d3bb23b2e83d36d63a6a6d4154a38a9bc03c03 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Wed, 26 Nov 2025 15:37:12 -0500 Subject: [PATCH 3/3] `go fmt ./...` --- internal/cmd/webhook/list.go | 1 - internal/cmd/webhook/list_test.go | 1 - internal/cmd/webhook/webhook.go | 1 - internal/mock/webhook.go | 1 - 4 files changed, 4 deletions(-) diff --git a/internal/cmd/webhook/list.go b/internal/cmd/webhook/list.go index 556f3d07..72d5ad9f 100644 --- a/internal/cmd/webhook/list.go +++ b/internal/cmd/webhook/list.go @@ -53,4 +53,3 @@ func ListCmd(ch *cmdutil.Helper) *cobra.Command { return cmd } - diff --git a/internal/cmd/webhook/list_test.go b/internal/cmd/webhook/list_test.go index 341fc64c..03b1a693 100644 --- a/internal/cmd/webhook/list_test.go +++ b/internal/cmd/webhook/list_test.go @@ -108,4 +108,3 @@ func TestWebhook_ListCmd_Empty(t *testing.T) { c.Assert(svc.ListFnInvoked, qt.IsTrue) c.Assert(buf.String(), qt.Contains, "No webhooks exist") } - diff --git a/internal/cmd/webhook/webhook.go b/internal/cmd/webhook/webhook.go index 4d379997..6b009b88 100644 --- a/internal/cmd/webhook/webhook.go +++ b/internal/cmd/webhook/webhook.go @@ -60,4 +60,3 @@ func toWebhooks(webhooks []*ps.Webhook) []*Webhook { } return results } - diff --git a/internal/mock/webhook.go b/internal/mock/webhook.go index 049261dd..34414671 100644 --- a/internal/mock/webhook.go +++ b/internal/mock/webhook.go @@ -55,4 +55,3 @@ func (w *WebhooksService) Test(ctx context.Context, req *ps.TestWebhookRequest) w.TestFnInvoked = true return w.TestFn(ctx, req) } -