Skip to content
Open
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
70 changes: 54 additions & 16 deletions internal/snippets/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"strconv"
"strings"

"github.com/go-errors/errors"
Expand All @@ -13,13 +14,38 @@ import (
"github.com/supabase/cli/pkg/api"
)

const defaultLimit = 10

func Run(ctx context.Context, fsys afero.Fs) error {
opts := api.V1ListAllSnippetsParams{ProjectRef: &flags.ProjectRef}
resp, err := utils.GetSupabase().V1ListAllSnippetsWithResponse(ctx, &opts)
if err != nil {
return errors.Errorf("failed to list snippets: %w", err)
} else if resp.JSON200 == nil {
return errors.Errorf("unexpected list snippets status %d: %s", resp.StatusCode(), string(resp.Body))
currentCursor := ""
var allResponses []*api.SnippetList

for {
limitStr := strconv.Itoa(defaultLimit)

opts := api.V1ListAllSnippetsParams{
ProjectRef: &flags.ProjectRef,
Limit: &limitStr,
}

if currentCursor != "" {
opts.Cursor = &currentCursor
}

resp, err := utils.GetSupabase().V1ListAllSnippetsWithResponse(ctx, &opts)
if err != nil {
return errors.Errorf("failed to list snippets: %w", err)
} else if resp.JSON200 == nil {
return errors.Errorf("unexpected list snippets status %d: %s", resp.StatusCode(), string(resp.Body))
}

allResponses = append(allResponses, resp.JSON200)

if resp.JSON200.Cursor == nil || *resp.JSON200.Cursor == "" {
break
}

currentCursor = *resp.JSON200.Cursor
}

switch utils.OutputFormat.Value {
Expand All @@ -28,20 +54,32 @@ func Run(ctx context.Context, fsys afero.Fs) error {
table.WriteString(`|ID|NAME|VISIBILITY|OWNER|CREATED AT (UTC)|UPDATED AT (UTC)|
|-|-|-|-|-|-|
`)
for _, snippet := range resp.JSON200.Data {
fmt.Fprintf(&table, "|`%s`|`%s`|`%s`|`%s`|`%s`|`%s`|\n",
snippet.Id,
strings.ReplaceAll(snippet.Name, "|", "\\|"),
strings.ReplaceAll(string(snippet.Visibility), "|", "\\|"),
strings.ReplaceAll(snippet.Owner.Username, "|", "\\|"),
utils.FormatTimestamp(snippet.InsertedAt),
utils.FormatTimestamp(snippet.UpdatedAt),
)
for _, resp := range allResponses {
for _, snippet := range resp.Data {
fmt.Fprintf(&table, "|`%s`|`%s`|`%s`|`%s`|`%s`|`%s`|\n",
snippet.Id,
strings.ReplaceAll(snippet.Name, "|", "\\|"),
strings.ReplaceAll(string(snippet.Visibility), "|", "\\|"),
strings.ReplaceAll(snippet.Owner.Username, "|", "\\|"),
utils.FormatTimestamp(snippet.InsertedAt),
utils.FormatTimestamp(snippet.UpdatedAt),
)
}
}
return utils.RenderTable(table.String())
case utils.OutputEnv:
return errors.New(utils.ErrEnvNotSupported)
}

return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, *resp.JSON200)
// Flatten all snippets for JSON/TOML output
var allSnippets []interface{}
for _, resp := range allResponses {
for _, snippet := range resp.Data {
allSnippets = append(allSnippets, snippet)
}
}

return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, map[string]interface{}{
"data": allSnippets,
})
}
128 changes: 121 additions & 7 deletions internal/snippets/list/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package list

import (
"context"
"io"
"net/http"
"os"
"testing"

"github.com/go-errors/errors"
Expand All @@ -16,21 +18,30 @@ import (
"github.com/supabase/cli/pkg/api"
)

func muteStdout(t *testing.T) func() {
r, w, err := os.Pipe()
assert.NoError(t, err)
oldStdout := os.Stdout
os.Stdout = w
return func() {
os.Stdout = oldStdout
assert.NoError(t, w.Close())
_, err := io.ReadAll(r)
assert.NoError(t, err)
assert.NoError(t, r.Close())
}
}

func TestListSnippets(t *testing.T) {
flags.ProjectRef = apitest.RandomProjectRef()

t.Run("lists sql snippets", func(t *testing.T) {
t.Cleanup(fstest.MockStdout(t, `

ID | NAME | VISIBILITY | OWNER | CREATED AT (UTC) | UPDATED AT (UTC)
--------------|--------------|------------|----------|---------------------|---------------------
test-snippet | Create table | user | supaseed | 2023-10-13 17:48:58 | 2023-10-13 17:48:58

`))
t.Cleanup(muteStdout(t))
t.Cleanup(apitest.MockPlatformAPI(t))
// Setup mock api
gock.New(utils.DefaultApiHost).
Get("v1/snippets").
MatchParam("project_ref", flags.ProjectRef).
Reply(http.StatusOK).
JSON(api.SnippetList{Data: []struct {
Description nullable.Nullable[string] `json:"description"`
Expand Down Expand Up @@ -127,4 +138,107 @@ func TestListSnippets(t *testing.T) {
err := Run(context.Background(), nil)
assert.ErrorContains(t, err, "unexpected list snippets status 503:")
})

t.Run("paginates through multiple pages", func(t *testing.T) {
t.Cleanup(muteStdout(t))
t.Cleanup(apitest.MockPlatformAPI(t))

// First page: 2 snippets + cursor for next page
cursor2 := "page2-cursor"
gock.New(utils.DefaultApiHost).
Get("v1/snippets").
MatchParam("project_ref", flags.ProjectRef).
Reply(http.StatusOK).
JSON(api.SnippetList{
Cursor: &cursor2,
Data: []struct {
Description nullable.Nullable[string] `json:"description"`
Favorite bool `json:"favorite"`
Id string `json:"id"`
InsertedAt string `json:"inserted_at"`
Name string `json:"name"`
Owner struct {
Id float32 `json:"id"`
Username string `json:"username"`
} `json:"owner"`
Project struct {
Id float32 `json:"id"`
Name string `json:"name"`
} `json:"project"`
Type api.SnippetListDataType `json:"type"`
UpdatedAt string `json:"updated_at"`
UpdatedBy struct {
Id float32 `json:"id"`
Username string `json:"username"`
} `json:"updated_by"`
Visibility api.SnippetListDataVisibility `json:"visibility"`
}{{
Id: "snippet-1",
Name: "Snippet 1",
Visibility: api.SnippetListDataVisibilityUser,
Owner: struct {
Id float32 `json:"id"`
Username string `json:"username"`
}{Username: "user"},
InsertedAt: "2023-10-13T17:48:58.491Z",
UpdatedAt: "2023-10-13T17:48:58.491Z",
}, {
Id: "snippet-2",
Name: "Snippet 2",
Visibility: api.SnippetListDataVisibilityUser,
Owner: struct {
Id float32 `json:"id"`
Username string `json:"username"`
}{Username: "user"},
InsertedAt: "2023-10-13T17:48:58.491Z",
UpdatedAt: "2023-10-13T17:48:58.491Z",
}},
})

// Second page: 1 snippet, no cursor (end of results)
gock.New(utils.DefaultApiHost).
Get("v1/snippets").
MatchParam("project_ref", flags.ProjectRef).
MatchParam("cursor", cursor2).
Reply(http.StatusOK).
JSON(api.SnippetList{
Cursor: nil,
Data: []struct {
Description nullable.Nullable[string] `json:"description"`
Favorite bool `json:"favorite"`
Id string `json:"id"`
InsertedAt string `json:"inserted_at"`
Name string `json:"name"`
Owner struct {
Id float32 `json:"id"`
Username string `json:"username"`
} `json:"owner"`
Project struct {
Id float32 `json:"id"`
Name string `json:"name"`
} `json:"project"`
Type api.SnippetListDataType `json:"type"`
UpdatedAt string `json:"updated_at"`
UpdatedBy struct {
Id float32 `json:"id"`
Username string `json:"username"`
} `json:"updated_by"`
Visibility api.SnippetListDataVisibility `json:"visibility"`
}{{
Id: "snippet-3",
Name: "Snippet 3",
Visibility: api.SnippetListDataVisibilityUser,
Owner: struct {
Id float32 `json:"id"`
Username string `json:"username"`
}{Username: "user"},
InsertedAt: "2023-10-13T17:48:58.491Z",
UpdatedAt: "2023-10-13T17:48:58.491Z",
}},
})

// Run test - should fetch 3 snippets across 2 pages
err := Run(context.Background(), nil)
assert.NoError(t, err)
})
}
Loading