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
92 changes: 92 additions & 0 deletions cmd/list_operations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package cmd

import (
"errors"
"os"
"sort"
"time"

"github.com/amp-labs/cli/clerk"
"github.com/amp-labs/cli/flags"
"github.com/amp-labs/cli/logger"
"github.com/amp-labs/cli/request"
"github.com/amp-labs/cli/utils"
"github.com/spf13/cobra"
)

type operationSummary struct {
Id string `json:"id"`
Action string `json:"action"`
Status string `json:"status"`
ReadType string `json:"readType,omitempty"`
StartedAt string `json:"startedAt"`
CompletedAt *string `json:"completedAt"`
}

func summarizeOperations(operations []*request.Operation) []operationSummary {
summaries := make([]operationSummary, 0, len(operations))

for _, operation := range operations {
var completedAt *string

if operation.UpdateTime != nil {
formatted := operation.UpdateTime.Format(time.RFC3339)
completedAt = &formatted
}

summaries = append(summaries, operationSummary{
Id: operation.Id,
Action: operation.ActionType,
Status: operation.Status,
ReadType: operation.ReadType,
StartedAt: operation.CreateTime.Format(time.RFC3339),
CompletedAt: completedAt,
})
}

return summaries
}

var listOperationsCmd = &cobra.Command{ //nolint:gochecknoglobals
Use: "list:operations <integrationId> <installationId>",
Short: "List operations for an installation",
Long: "List recent operations for an installation.",
Args: cobra.ExactArgs(2), //nolint:mnd
Run: func(cmd *cobra.Command, args []string) {
projectId := flags.GetProjectOrFail()
apiKey := flags.GetAPIKey()
client := request.NewAPIClient(projectId, &apiKey)

operations, err := client.ListOperations(cmd.Context(), args[0], args[1])
if err != nil {
if errors.Is(err, clerk.ErrNoSessions) {
logger.FatalErr("Authenticated session has expired, please log in using amp login", err)
} else {
logger.FatalErr("Unable to list operations", err)
}
}

sort.Slice(operations, func(i, j int) bool {
return operations[i].CreateTime.After(operations[j].CreateTime)
})

format, err := cmd.Flags().GetString("format")
if err != nil {
logger.FatalErr("Unable to read output format", err)
}

err = utils.WriteStruct(os.Stdout, utils.Format(format), summarizeOperations(operations))
if err != nil {
logger.FatalErr("Unable to write operations", err)
}
},
}

func init() {
err := flags.InitAndBindFormatFlag(listOperationsCmd)
if err != nil {
logger.FatalErr("unable to initialize flags", err)
}

rootCmd.AddCommand(listOperationsCmd)
}
46 changes: 46 additions & 0 deletions cmd/list_operations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package cmd

import (
"reflect"
"testing"
"time"

"github.com/amp-labs/cli/request"
)

func TestSummarizeOperationsOmitsProviderResource(t *testing.T) {
t.Parallel()

startedAt := time.Date(2026, time.August, 19, 12, 0, 0, 0, time.UTC)
completedAt := startedAt.Add(time.Minute)
operations := []*request.Operation{
{
Id: "operation-id",
ActionType: "proxy",
Status: "success",
Resource: "/1.0/projects/provider-record-id",
CreateTime: startedAt,
UpdateTime: &completedAt,
},
}

got := summarizeOperations(operations)
wantCompletedAt := "2026-08-19T12:01:00Z"
want := []operationSummary{
{
Id: "operation-id",
Action: "proxy",
Status: "success",
StartedAt: "2026-08-19T12:00:00Z",
CompletedAt: &wantCompletedAt,
},
}

if !reflect.DeepEqual(got, want) {
t.Fatalf("summarizeOperations() = %#v, want %#v", got, want)
}

if reflect.ValueOf(got[0]).FieldByName("Resource").IsValid() {
t.Fatal("operation summary includes provider resource")
}
}
25 changes: 25 additions & 0 deletions request/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,31 @@ func (c *APIClient) ListInstallations(ctx context.Context, integrationId string)
return installations, nil
}

func (c *APIClient) ListOperations(
ctx context.Context, integrationId string, installationId string,
) ([]*Operation, error) {
listURL := fmt.Sprintf(
"%s/projects/%s/integrations/%s/installations/%s/operations",
c.Root, c.ProjectId, integrationId, installationId,
)

auth, err := c.getAuthHeader(ctx)
if err != nil {
return nil, err
}

response := struct {
Results []*Operation `json:"results"`
}{}

_, err = c.Client.Get(ctx, listURL, &response, auth) //nolint:bodyclose
if err != nil {
return nil, err
}

return response.Results, nil
}

func (c *APIClient) ListConnections(ctx context.Context) ([]*Connection, error) {
listURL := fmt.Sprintf("%s/projects/%s/connections", c.Root, c.ProjectId)

Expand Down
74 changes: 74 additions & 0 deletions request/list_operations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package request

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
)

func TestListOperations(t *testing.T) {
t.Parallel()

startedAt := time.Date(2026, time.August, 19, 0, 54, 26, 0, time.UTC)
completedAt := startedAt.Add(time.Second)

server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet ||
req.URL.Path != "/v1/projects/test-project/integrations/integration-id/installations/installation-id/operations" {
t.Errorf("request = %s %s, want GET operations path", req.Method, req.URL.Path)
}

if req.Header.Get("X-Api-Key") != "test-api-key" {
t.Errorf("X-Api-Key = %q, want test-api-key", req.Header.Get("X-Api-Key"))
}

writer.Header().Set("Content-Type", "application/json")

err := json.NewEncoder(writer).Encode(map[string]any{
"results": []Operation{
{
Id: "operation-id",
InstallationId: "installation-id",
ActionType: "read",
Status: "success",
Resource: "projects",
ReadType: "scheduled",
CreateTime: startedAt,
UpdateTime: &completedAt,
},
},
"pagination": map[string]any{
"done": true,
"nextPageToken": "",
},
})
if err != nil {
t.Errorf("encode response: %v", err)
}
}))
defer server.Close()

apiKey := "test-api-key"
client := &APIClient{
Root: server.URL + "/v1",
ProjectId: "test-project",
APIKey: &apiKey,
Client: &Client{Client: server.Client()},
}

operations, err := client.ListOperations(t.Context(), "integration-id", "installation-id")
if err != nil {
t.Fatalf("list operations: %v", err)
}

if len(operations) != 1 {
t.Fatalf("operations count = %d, want 1", len(operations))
}

operation := operations[0]
if operation.Id != "operation-id" || operation.Status != "success" || operation.Resource != "projects" {
t.Fatalf("operation = %#v", operation)
}
}
11 changes: 11 additions & 0 deletions request/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ type Installation struct {
HealthStatus string `json:"healthStatus"`
}

type Operation struct {
Id string `json:"id"`
InstallationId string `json:"installationId"`
ActionType string `json:"actionType"`
Status string `json:"status"`
Resource string `json:"resource"`
ReadType string `json:"readType"`
CreateTime time.Time `json:"createTime"`
UpdateTime *time.Time `json:"updateTime"`
}

type Group struct {
GroupRef string `json:"groupRef"`
GroupName string `json:"groupName"`
Expand Down
Loading