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 go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ 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.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=
Expand Down
5 changes: 5 additions & 0 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
55 changes: 55 additions & 0 deletions internal/cmd/webhook/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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 <database>",
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
}
110 changes: 110 additions & 0 deletions internal/cmd/webhook/list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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")
}
62 changes: 62 additions & 0 deletions internal/cmd/webhook/webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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 <command>",
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
}
57 changes: 57 additions & 0 deletions internal/mock/webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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)
}