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
16 changes: 16 additions & 0 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,19 @@ func (c *Client) DeleteRecord(ctx context.Context, objectName, recordID string)
func (c *Client) RecordURL(recordID string) string {
return fmt.Sprintf("%s/%s", c.InstanceURL, recordID)
}

// Search executes a SOSL search and returns the results
func (c *Client) Search(ctx context.Context, sosl string) (*SearchResult, error) {
path := fmt.Sprintf("/search?q=%s", url.QueryEscape(sosl))
body, err := c.Get(ctx, path)
if err != nil {
return nil, err
}

var result SearchResult
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse search result: %w", err)
}

return &result, nil
}
43 changes: 43 additions & 0 deletions api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,46 @@ type LimitInfo struct {
Max int `json:"Max"`
Remaining int `json:"Remaining"`
}

// SearchResult represents the result of a SOSL search
type SearchResult struct {
SearchRecords []SearchRecord `json:"searchRecords"`
}

// SearchRecord represents a single search result record
type SearchRecord struct {
Attributes SObjectAttributes `json:"attributes"`
ID string `json:"Id"`
Fields map[string]interface{} `json:"-"`
}

// UnmarshalJSON custom unmarshaler for SearchRecord to capture all fields
func (s *SearchRecord) UnmarshalJSON(data []byte) error {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return err
}

s.Fields = make(map[string]interface{})

for key, value := range raw {
switch key {
case "attributes":
if err := json.Unmarshal(value, &s.Attributes); err != nil {
return err
}
case "Id":
if err := json.Unmarshal(value, &s.ID); err != nil {
return err
}
default:
var v interface{}
if err := json.Unmarshal(value, &v); err != nil {
return err
}
s.Fields[key] = v
}
}

return nil
}
12 changes: 12 additions & 0 deletions cmd/sfdc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import (
"github.com/open-cli-collective/salesforce-cli/internal/cmd/completion"
"github.com/open-cli-collective/salesforce-cli/internal/cmd/configcmd"
"github.com/open-cli-collective/salesforce-cli/internal/cmd/initcmd"
"github.com/open-cli-collective/salesforce-cli/internal/cmd/limitscmd"
"github.com/open-cli-collective/salesforce-cli/internal/cmd/objectcmd"
"github.com/open-cli-collective/salesforce-cli/internal/cmd/querycmd"
"github.com/open-cli-collective/salesforce-cli/internal/cmd/recordcmd"
"github.com/open-cli-collective/salesforce-cli/internal/cmd/root"
"github.com/open-cli-collective/salesforce-cli/internal/cmd/searchcmd"
)

// Exit codes
Expand All @@ -33,5 +38,12 @@ func run() error {
configcmd.Register(rootCmd, opts)
completion.Register(rootCmd, opts)

// REST API commands
querycmd.Register(rootCmd, opts)
recordcmd.Register(rootCmd, opts)
searchcmd.Register(rootCmd, opts)
objectcmd.Register(rootCmd, opts)
limitscmd.Register(rootCmd, opts)

return rootCmd.Execute()
}
129 changes: 129 additions & 0 deletions internal/cmd/limitscmd/limits.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Package limitscmd provides the limits command for viewing org API limits.
package limitscmd

import (
"context"
"fmt"
"sort"

"github.com/spf13/cobra"

"github.com/open-cli-collective/salesforce-cli/api"
"github.com/open-cli-collective/salesforce-cli/internal/cmd/root"
)

// Register registers the limits command with the root command.
func Register(parent *cobra.Command, opts *root.Options) {
parent.AddCommand(NewCommand(opts))
}

// NewCommand creates the limits command.
func NewCommand(opts *root.Options) *cobra.Command {
var show string

cmd := &cobra.Command{
Use: "limits",
Short: "Display org API limits",
Long: `Display the current Salesforce org's API limits and usage.

Examples:
sfdc limits
sfdc limits -o json
sfdc limits --show DailyApiRequests`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runLimits(cmd.Context(), opts, show)
},
}

cmd.Flags().StringVar(&show, "show", "", "Show only a specific limit by name")

return cmd
}

func runLimits(ctx context.Context, opts *root.Options, show string) error {
client, err := opts.APIClient()
if err != nil {
return fmt.Errorf("failed to create API client: %w", err)
}

limits, err := client.GetLimits(ctx)
if err != nil {
return fmt.Errorf("failed to get limits: %w", err)
}

// If showing a specific limit
if show != "" {
return renderSingleLimit(opts, limits, show)
}

return renderLimits(opts, limits)
}

func renderSingleLimit(opts *root.Options, limits api.Limits, name string) error {
v := opts.View()

limit, ok := limits[name]
if !ok {
return fmt.Errorf("limit %q not found", name)
}

if opts.Output == "json" {
return v.JSON(map[string]interface{}{
"name": name,
"max": limit.Max,
"remaining": limit.Remaining,
"used": limit.Max - limit.Remaining,
})
}

used := limit.Max - limit.Remaining
pct := float64(0)
if limit.Max > 0 {
pct = float64(used) / float64(limit.Max) * 100
}

v.Info("%s", name)
v.Info(" Max: %d", limit.Max)
v.Info(" Remaining: %d", limit.Remaining)
v.Info(" Used: %d (%.1f%%)", used, pct)

return nil
}

func renderLimits(opts *root.Options, limits api.Limits) error {
v := opts.View()

if opts.Output == "json" {
return v.JSON(limits)
}

// Sort limit names for consistent output
names := make([]string, 0, len(limits))
for name := range limits {
names = append(names, name)
}
sort.Strings(names)

headers := []string{"Limit", "Max", "Remaining", "Used", "Usage %"}
rows := make([][]string, 0, len(names))

for _, name := range names {
limit := limits[name]
used := limit.Max - limit.Remaining
pct := float64(0)
if limit.Max > 0 {
pct = float64(used) / float64(limit.Max) * 100
}

rows = append(rows, []string{
name,
fmt.Sprintf("%d", limit.Max),
fmt.Sprintf("%d", limit.Remaining),
fmt.Sprintf("%d", used),
fmt.Sprintf("%.1f%%", pct),
})
}

return v.Table(headers, rows)
}
163 changes: 163 additions & 0 deletions internal/cmd/limitscmd/limits_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package limitscmd

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/open-cli-collective/salesforce-cli/api"
"github.com/open-cli-collective/salesforce-cli/internal/cmd/root"
)

func TestLimitsCommand(t *testing.T) {
limits := api.Limits{
"DailyApiRequests": api.LimitInfo{Max: 100000, Remaining: 99500},
"DailyBulkApiRequests": api.LimitInfo{Max: 10000, Remaining: 10000},
}

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Contains(t, r.URL.Path, "/limits")
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(limits)
}))
defer server.Close()

client, err := api.New(api.ClientConfig{
InstanceURL: server.URL,
HTTPClient: server.Client(),
})
require.NoError(t, err)

stdout := &bytes.Buffer{}
opts := &root.Options{
Output: "table",
Stdout: stdout,
Stderr: &bytes.Buffer{},
}
opts.SetAPIClient(client)

cmd := NewCommand(opts)
cmd.SetOut(stdout)

err = cmd.Execute()
require.NoError(t, err)

output := stdout.String()
assert.Contains(t, output, "DailyApiRequests")
assert.Contains(t, output, "100000")
assert.Contains(t, output, "99500")
}

func TestLimitsCommand_ShowSpecific(t *testing.T) {
limits := api.Limits{
"DailyApiRequests": api.LimitInfo{Max: 100000, Remaining: 99500},
"DailyBulkApiRequests": api.LimitInfo{Max: 10000, Remaining: 10000},
}

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(limits)
}))
defer server.Close()

client, err := api.New(api.ClientConfig{
InstanceURL: server.URL,
HTTPClient: server.Client(),
})
require.NoError(t, err)

stdout := &bytes.Buffer{}
opts := &root.Options{
Output: "table",
Stdout: stdout,
Stderr: &bytes.Buffer{},
}
opts.SetAPIClient(client)

cmd := NewCommand(opts)
cmd.SetArgs([]string{"--show", "DailyApiRequests"})
cmd.SetOut(stdout)

err = cmd.Execute()
require.NoError(t, err)

output := stdout.String()
assert.Contains(t, output, "DailyApiRequests")
assert.Contains(t, output, "Max:")
assert.Contains(t, output, "Remaining:")
assert.Contains(t, output, "Used:")
}

func TestLimitsCommand_ShowNotFound(t *testing.T) {
limits := api.Limits{
"DailyApiRequests": api.LimitInfo{Max: 100000, Remaining: 99500},
}

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(limits)
}))
defer server.Close()

client, err := api.New(api.ClientConfig{
InstanceURL: server.URL,
HTTPClient: server.Client(),
})
require.NoError(t, err)

opts := &root.Options{
Output: "table",
Stdout: &bytes.Buffer{},
Stderr: &bytes.Buffer{},
}
opts.SetAPIClient(client)

cmd := NewCommand(opts)
cmd.SetArgs([]string{"--show", "NonExistentLimit"})

err = cmd.Execute()
assert.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}

func TestLimitsCommand_JSONOutput(t *testing.T) {
limits := api.Limits{
"DailyApiRequests": api.LimitInfo{Max: 100000, Remaining: 99500},
}

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(limits)
}))
defer server.Close()

client, err := api.New(api.ClientConfig{
InstanceURL: server.URL,
HTTPClient: server.Client(),
})
require.NoError(t, err)

stdout := &bytes.Buffer{}
opts := &root.Options{
Output: "json",
Stdout: stdout,
Stderr: &bytes.Buffer{},
}
opts.SetAPIClient(client)

cmd := NewCommand(opts)
cmd.SetOut(stdout)

err = cmd.Execute()
require.NoError(t, err)

var result api.Limits
err = json.Unmarshal(stdout.Bytes(), &result)
require.NoError(t, err)
assert.Equal(t, 100000, result["DailyApiRequests"].Max)
}
Loading