Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
56 changes: 56 additions & 0 deletions internal/cmd/webhook/list.go
Original file line number Diff line number Diff line change
@@ -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 <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
}

111 changes: 111 additions & 0 deletions internal/cmd/webhook/list_test.go
Original file line number Diff line number Diff line change
@@ -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")
}

63 changes: 63 additions & 0 deletions internal/cmd/webhook/webhook.go
Original file line number Diff line number Diff line change
@@ -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 <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
}

58 changes: 58 additions & 0 deletions internal/mock/webhook.go
Original file line number Diff line number Diff line change
@@ -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)
}

Loading