diff --git a/.gitignore b/.gitignore index 56a0327..982c322 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ Thumbs.db # Distribution dist/ +sfdc diff --git a/README.md b/README.md index 0488cf2..b2b6884 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,12 @@ choco install salesforce-cli winget install OpenCLICollective.salesforce-cli ``` +### Linux (Snap) + +```bash +sudo snap install ocli-sfdc +``` + ### Linux (deb/rpm) Download the appropriate package from the [releases page](https://github.com/open-cli-collective/salesforce-cli/releases). @@ -35,15 +41,22 @@ go install github.com/open-cli-collective/salesforce-cli/cmd/sfdc@latest ## Quick Start -1. Set up authentication: - ```bash - sfdc init - ``` +```bash +# Set up OAuth authentication +sfdc init + +# Query data +sfdc query "SELECT Id, Name FROM Account LIMIT 5" -2. Query data: - ```bash - sfdc query "SELECT Id, Name FROM Account LIMIT 5" - ``` +# Search across objects +sfdc search "Acme" + +# Get a record +sfdc record get Account 001xx000003DGbYAAW + +# Check org limits +sfdc limits +``` ## Configuration @@ -57,13 +70,270 @@ The CLI stores configuration in `~/.config/salesforce-cli/config.json` and OAuth | `SFDC_CLIENT_ID` | Connected App consumer key | | `SFDC_ACCESS_TOKEN` | Direct access token (bypasses OAuth) | +### Commands + +```bash +sfdc config show # Display current configuration +sfdc config test # Verify API connectivity +sfdc config clear # Remove stored credentials +``` + +## Global Flags + +All commands support these flags: + +| Flag | Description | +|------|-------------| +| `-o, --output` | Output format: `table`, `json`, `plain` (default: `table`) | +| `--no-color` | Disable colored output | +| `-v, --verbose` | Enable verbose output | +| `--api-version` | Salesforce API version (default: `v62.0`) | + ## Commands -- `sfdc init` - Set up OAuth authentication -- `sfdc config show` - Display current configuration -- `sfdc config test` - Verify API connectivity -- `sfdc config clear` - Remove stored credentials -- `sfdc completion` - Generate shell completion scripts +### Query & Search + +#### SOQL Query + +```bash +# Basic query +sfdc query "SELECT Id, Name FROM Account LIMIT 10" + +# Include deleted/archived records +sfdc query "SELECT Id, Name FROM Account" --all + +# Fetch all pages (large datasets) +sfdc query "SELECT Id, Name FROM Contact" --no-limit + +# JSON output +sfdc query "SELECT Id, Name, Phone FROM Contact" -o json +``` + +#### SOSL Search + +```bash +# Simple search +sfdc search "Acme" + +# Limit to specific objects +sfdc search "John Smith" --in Account,Contact + +# Specify return fields +sfdc search "test" --returning "Account(Id,Name),Contact(Id,FirstName,LastName)" + +# Full SOSL syntax +sfdc search "FIND {Acme} IN ALL FIELDS RETURNING Account(Id,Name)" +``` + +### Records + +```bash +# Get a record +sfdc record get Account 001xx000003DGbYAAW +sfdc record get Contact 003xx000001abcd --fields Name,Email,Phone + +# Create a record +sfdc record create Account --set Name="Acme Corp" +sfdc record create Contact --set FirstName=John --set LastName=Doe --set Email=john@example.com + +# Update a record +sfdc record update Account 001xx000003DGbYAAW --set Name="New Name" +sfdc record update Contact 003xx000001abcd --set Phone="555-1234" --set Email=new@example.com + +# Delete a record +sfdc record delete Account 001xx000003DGbYAAW --confirm +``` + +### Objects + +```bash +# List all objects +sfdc object list +sfdc object list --custom-only + +# Describe object metadata +sfdc object describe Account + +# List fields +sfdc object fields Account +sfdc object fields Account --required-only +``` + +### Org Limits + +```bash +# Show all limits +sfdc limits + +# Show specific limit +sfdc limits --show DailyApiRequests +``` + +### Bulk API 2.0 + +For large data operations (thousands or millions of records). + +#### Import + +```bash +# Insert records +sfdc bulk import Account --file accounts.csv --operation insert + +# Update records (requires Id column) +sfdc bulk import Account --file accounts.csv --operation update + +# Upsert with external ID +sfdc bulk import Contact --file contacts.csv --operation upsert --external-id Email + +# Delete records (requires Id column) +sfdc bulk import Account --file delete-ids.csv --operation delete + +# Wait for completion +sfdc bulk import Account --file accounts.csv --operation insert --wait +``` + +#### Export + +```bash +# Export to stdout +sfdc bulk export "SELECT Id, Name, Industry FROM Account" + +# Export to file +sfdc bulk export "SELECT Id, Name FROM Account" --output accounts.csv +sfdc bulk export "SELECT * FROM Contact" --output contacts.csv +``` + +#### Job Management + +```bash +# List recent jobs +sfdc bulk job list + +# Check job status +sfdc bulk job status 750xx000000001 + +# Get successful results +sfdc bulk job results 750xx000000001 +sfdc bulk job results 750xx000000001 --output results.csv + +# Get failed records +sfdc bulk job errors 750xx000000001 +sfdc bulk job errors 750xx000000001 --output errors.csv + +# Abort a job +sfdc bulk job abort 750xx000000001 +``` + +### Apex (Tooling API) + +#### List & Get Source + +```bash +# List Apex classes +sfdc apex list +sfdc apex list --triggers + +# Get source code +sfdc apex get MyController +sfdc apex get MyController --output MyController.cls +sfdc apex get MyTrigger --trigger +``` + +#### Execute Anonymous Apex + +```bash +# From argument +sfdc apex execute "System.debug('Hello');" + +# From file +sfdc apex execute --file script.apex + +# From stdin +echo "System.debug(UserInfo.getUserName());" | sfdc apex execute - +``` + +#### Run Tests + +```bash +# Run all tests in a class +sfdc apex test --class MyControllerTest + +# Run specific method +sfdc apex test --class MyControllerTest --method testCreate + +# Wait for completion +sfdc apex test --class MyTest --wait +``` + +### Debug Logs + +```bash +# List recent logs +sfdc log list +sfdc log list --limit 20 +sfdc log list --user 005xxx + +# Get log content +sfdc log get 07L1x000000ABCD +sfdc log get 07L1x000000ABCD --output debug.log + +# Stream new logs (Ctrl+C to stop) +sfdc log tail +sfdc log tail --user 005xxx +sfdc log tail --interval 5 +``` + +### Code Coverage + +```bash +# Show all coverage +sfdc coverage + +# Coverage for specific class +sfdc coverage --class MyController + +# Fail if below threshold +sfdc coverage --min 75 +``` + +### Metadata API + +Basic metadata operations. For complex workflows, use the official Salesforce CLI (sf). + +```bash +# List available metadata types +sfdc metadata types + +# List components of a type +sfdc metadata list --type ApexClass +sfdc metadata list --type ApexTrigger + +# Retrieve components +sfdc metadata retrieve --type ApexClass --output ./src +sfdc metadata retrieve --type ApexClass --name MyController --output ./src + +# Deploy from directory +sfdc metadata deploy --source ./src +sfdc metadata deploy --source ./src --check-only +sfdc metadata deploy --source ./src --test-level RunLocalTests +sfdc metadata deploy --source ./src --wait +``` + +### Shell Completion + +```bash +# Bash +source <(sfdc completion bash) + +# Zsh +sfdc completion zsh > "${fpath[1]}/_sfdc" + +# Fish +sfdc completion fish | source + +# PowerShell +sfdc completion powershell | Out-String | Invoke-Expression +``` ## License diff --git a/api/client.go b/api/client.go index 077d2c1..8941df2 100644 --- a/api/client.go +++ b/api/client.go @@ -50,10 +50,8 @@ func New(cfg ClientConfig) (*Client, error) { return nil, ErrHTTPClientRequired } - // Normalize instance URL instanceURL := normalizeURL(cfg.InstanceURL) - // Use default API version if not specified apiVersion := cfg.APIVersion if apiVersion == "" { apiVersion = DefaultAPIVersion @@ -71,12 +69,10 @@ func New(cfg ClientConfig) (*Client, error) { func normalizeURL(urlStr string) string { urlStr = strings.TrimSpace(urlStr) - // Add https:// if no scheme provided if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") { urlStr = "https://" + urlStr } - // Remove trailing slash return strings.TrimSuffix(urlStr, "/") } @@ -105,12 +101,9 @@ func (c *Client) Delete(ctx context.Context, path string) ([]byte, error) { return c.doRequest(ctx, http.MethodDelete, path, nil) } -// doRequest performs an HTTP request func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}) ([]byte, error) { - // Build full URL fullURL := c.buildURL(path) - // Prepare request body var bodyReader io.Reader if body != nil { jsonBody, err := json.Marshal(body) @@ -120,28 +113,23 @@ func (c *Client) doRequest(ctx context.Context, method, path string, body interf bodyReader = bytes.NewReader(jsonBody) } - // Create request req, err := http.NewRequestWithContext(ctx, method, fullURL, bodyReader) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } - // Set headers req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") - // Execute request resp, err := c.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } - // Check for errors if resp.StatusCode >= 400 { return nil, ParseAPIError(resp) } - // Read response body defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { @@ -151,19 +139,15 @@ func (c *Client) doRequest(ctx context.Context, method, path string, body interf return respBody, nil } -// buildURL constructs the full URL for an API path func (c *Client) buildURL(path string) string { - // If path is already absolute, use it directly if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") { return path } - // If path starts with /services/, it's a full path from instance root if strings.HasPrefix(path, "/services/") { return c.InstanceURL + path } - // Otherwise, it's relative to the API base URL if !strings.HasPrefix(path, "/") { path = "/" + path } @@ -268,7 +252,6 @@ func (c *Client) QueryAll(ctx context.Context, soql string) (*QueryResult, error return nil, err } - // Fetch additional pages if needed for !result.Done && result.NextRecordsURL != "" { nextPage, err := c.QueryMore(ctx, result.NextRecordsURL) if err != nil { diff --git a/api/metadata/client.go b/api/metadata/client.go index ce13700..7679f07 100644 --- a/api/metadata/client.go +++ b/api/metadata/client.go @@ -109,16 +109,13 @@ func (c *Client) Post(ctx context.Context, path string, body interface{}) ([]byt } // DescribeMetadata returns available metadata types. -// Uses the Tooling API to get metadata type information. func (c *Client) DescribeMetadata(ctx context.Context) (*DescribeMetadataResult, error) { - // Use Tooling API's describe endpoint for metadata types path := "/tooling/describe" body, err := c.Get(ctx, path) if err != nil { return nil, err } - // Parse the describe result to extract metadata types var describeResult struct { Sobjects []struct { Name string `json:"name"` @@ -134,8 +131,6 @@ func (c *Client) DescribeMetadata(ctx context.Context) (*DescribeMetadataResult, return nil, fmt.Errorf("failed to parse describe result: %w", err) } - // Filter and convert to MetadataType - // Common metadata types that can be queried via Tooling API metadataTypeNames := map[string]bool{ "ApexClass": true, "ApexTrigger": true, @@ -169,7 +164,6 @@ func (c *Client) DescribeMetadata(ctx context.Context) (*DescribeMetadataResult, // ListMetadata lists components of a specific metadata type. func (c *Client) ListMetadata(ctx context.Context, metadataType string) ([]MetadataComponent, error) { - // Query the Tooling API for the metadata type var soql string switch metadataType { case "ApexClass": @@ -216,7 +210,6 @@ func (c *Client) ListMetadata(ctx context.Context, metadataType string) ([]Metad comp.ID = id } - // Handle different name fields if name, ok := rec["Name"].(string); ok { comp.FullName = name } else if name, ok := rec["DeveloperName"].(string); ok { @@ -235,7 +228,6 @@ func (c *Client) ListMetadata(ctx context.Context, metadataType string) ([]Metad // Deploy deploys metadata to the org. func (c *Client) Deploy(ctx context.Context, zipData []byte, options DeployOptions) (*DeployResult, error) { - // Encode zip as base64 zipBase64 := base64.StdEncoding.EncodeToString(zipData) request := DeployRequest{ @@ -286,27 +278,22 @@ func CreateZipFromDirectory(sourceDir string) ([]byte, error) { return err } - // Get relative path relPath, err := filepath.Rel(sourceDir, path) if err != nil { return err } - // Skip the root directory itself if relPath == "." { return nil } - // Use forward slashes for zip paths zipPath := filepath.ToSlash(relPath) if info.IsDir() { - // Add directory entry _, err := zipWriter.Create(zipPath + "/") return err } - // Add file writer, err := zipWriter.Create(zipPath) if err != nil { return err @@ -341,10 +328,9 @@ func ExtractZipToDirectory(zipData []byte, destDir string) error { } for _, file := range reader.File { - // Construct destination path destPath := filepath.Join(destDir, file.Name) - // Check for zip slip vulnerability + // Prevent zip slip vulnerability if !strings.HasPrefix(destPath, filepath.Clean(destDir)+string(os.PathSeparator)) { return fmt.Errorf("illegal file path: %s", file.Name) } @@ -356,12 +342,10 @@ func ExtractZipToDirectory(zipData []byte, destDir string) error { continue } - // Create parent directories if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { return err } - // Extract file if err := extractFile(file, destPath); err != nil { return err } @@ -388,11 +372,9 @@ func extractFile(file *zip.File, destPath string) error { return err } -// Retrieve retrieves metadata from the org. -// Note: The REST Metadata API retrieve endpoint is not fully supported in all orgs. -// For complex retrieves, consider using the Tooling API to get individual components. +// Retrieve retrieves metadata from the org using the Tooling API. +// For complex retrieves with package.xml, use the official Salesforce CLI. func (c *Client) Retrieve(ctx context.Context, metadataType, componentName string) ([]byte, error) { - // For simple cases, we can retrieve component body via Tooling API var soql string switch metadataType { case "ApexClass": @@ -425,7 +407,6 @@ func (c *Client) Retrieve(ctx context.Context, metadataType, componentName strin return nil, fmt.Errorf("%s not found: %s", metadataType, componentName) } - // Get the body/markup field rec := queryResult.Records[0] var content string if body, ok := rec["Body"].(string); ok { @@ -441,7 +422,6 @@ func (c *Client) Retrieve(ctx context.Context, metadataType, componentName strin // RetrieveAll retrieves all components of a type from the org. func (c *Client) RetrieveAll(ctx context.Context, metadataType string) (map[string][]byte, error) { - // First list all components components, err := c.ListMetadata(ctx, metadataType) if err != nil { return nil, err @@ -449,14 +429,12 @@ func (c *Client) RetrieveAll(ctx context.Context, metadataType string) (map[stri results := make(map[string][]byte) for _, comp := range components { - // Skip managed package components if comp.NamespacePrefix != "" { continue } content, err := c.Retrieve(ctx, metadataType, comp.FullName) if err != nil { - // Log error but continue with other components continue } results[comp.FullName] = content diff --git a/internal/cmd/bulkcmd/import.go b/internal/cmd/bulkcmd/import.go index 65d54be..3077bc3 100644 --- a/internal/cmd/bulkcmd/import.go +++ b/internal/cmd/bulkcmd/import.go @@ -55,27 +55,22 @@ Examples: } func runImport(ctx context.Context, opts *root.Options, object, file, operation, externalID string, wait bool) error { - // Validate operation op := bulk.Operation(strings.ToLower(operation)) switch op { case bulk.OperationInsert, bulk.OperationUpdate, bulk.OperationUpsert, bulk.OperationDelete: - // Valid default: return fmt.Errorf("invalid operation: %s (must be insert, update, upsert, or delete)", operation) } - // Upsert requires external ID if op == bulk.OperationUpsert && externalID == "" { return fmt.Errorf("--external-id is required for upsert operation") } - // Read CSV file data, err := os.ReadFile(file) if err != nil { return fmt.Errorf("failed to read file: %w", err) } - // Create bulk client client, err := opts.BulkClient() if err != nil { return fmt.Errorf("failed to create bulk client: %w", err) @@ -83,7 +78,6 @@ func runImport(ctx context.Context, opts *root.Options, object, file, operation, v := opts.View() - // Create job v.Info("Creating bulk %s job for %s...", operation, object) job, err := client.CreateJob(ctx, bulk.JobConfig{ Object: object, @@ -96,13 +90,11 @@ func runImport(ctx context.Context, opts *root.Options, object, file, operation, v.Info("Job created: %s", job.ID) - // Upload data v.Info("Uploading data...") if err := client.UploadJobData(ctx, job.ID, data); err != nil { return fmt.Errorf("failed to upload data: %w", err) } - // Close job to start processing v.Info("Starting job processing...") job, err = client.CloseJob(ctx, job.ID) if err != nil { @@ -114,14 +106,12 @@ func runImport(ctx context.Context, opts *root.Options, object, file, operation, return nil } - // Poll until complete v.Info("Waiting for job to complete...") job, err = client.PollJob(ctx, job.ID, bulk.DefaultPollConfig()) if err != nil { return fmt.Errorf("failed waiting for job: %w", err) } - // Show results return renderJobResult(opts, job) } diff --git a/internal/cmd/configcmd/configcmd.go b/internal/cmd/configcmd/configcmd.go index af2f95a..6ec401e 100644 --- a/internal/cmd/configcmd/configcmd.go +++ b/internal/cmd/configcmd/configcmd.go @@ -83,14 +83,12 @@ func runShow(cmd *cobra.Command, args []string) error { fmt.Println("============================") fmt.Println() - // Instance URL if cfg.InstanceURL != "" { fmt.Printf("Instance URL: %s\n", cfg.InstanceURL) } else { fmt.Println("Instance URL: Not configured") } - // Client ID (show partial for security) if cfg.ClientID != "" { masked := maskClientID(cfg.ClientID) fmt.Printf("Client ID: %s\n", masked) @@ -98,7 +96,6 @@ func runShow(cmd *cobra.Command, args []string) error { fmt.Println("Client ID: Not configured") } - // Token status fmt.Println() if keychain.HasStoredToken() { fmt.Printf("Token: Found (stored in %s)\n", keychain.GetStorageBackend()) @@ -106,7 +103,6 @@ func runShow(cmd *cobra.Command, args []string) error { fmt.Println("Token: Not found") } - // Config file location fmt.Println() configPath, err := config.GetConfigPath() if err != nil { @@ -130,14 +126,12 @@ func runTest(cmd *cobra.Command, args []string) error { fmt.Println("Testing Salesforce connection...") fmt.Println() - // Check token exists if !keychain.HasStoredToken() { fmt.Println(" Token: NOT FOUND") return fmt.Errorf("no OAuth token found - please run 'sfdc init' first") } fmt.Println(" Token: Found") - // Get authenticated client ctx := context.Background() client, err := auth.GetHTTPClient(ctx) if err != nil { @@ -146,7 +140,6 @@ func runTest(cmd *cobra.Command, args []string) error { } fmt.Println(" OAuth: OK") - // Test API access normalizedURL := normalizeURL(cfg.InstanceURL) resp, err := client.Get(normalizedURL + "/services/data/") if err != nil { @@ -181,13 +174,11 @@ func runClear(force bool) error { var hadToken, hadConfig bool var tokenErr, configErr error - // Clear token from keychain if keychain.HasStoredToken() { hadToken = true tokenErr = keychain.DeleteToken() } - // Clear config file cfg, _ := config.Load() if cfg.InstanceURL != "" || cfg.ClientID != "" { hadConfig = true @@ -196,7 +187,6 @@ func runClear(force bool) error { configErr = config.Save(cfg) } - // Report results if tokenErr != nil { fmt.Printf("Warning: failed to remove token: %v\n", tokenErr) } else if hadToken { diff --git a/internal/cmd/initcmd/init.go b/internal/cmd/initcmd/init.go index 61ab16a..f30168b 100644 --- a/internal/cmd/initcmd/init.go +++ b/internal/cmd/initcmd/init.go @@ -64,7 +64,6 @@ Prerequisites: func runInit(cmd *cobra.Command, args []string) error { reader := bufio.NewReader(os.Stdin) - // Step 1: Check existing configuration fmt.Println("Checking existing configuration...") cfg, _ := config.Load() @@ -80,7 +79,6 @@ func runInit(cmd *cobra.Command, args []string) error { return nil } - // Token exists but verification failed fmt.Println() fmt.Println("Your OAuth token appears to be expired or revoked.") fmt.Print("Would you like to re-authenticate? [Y/n]: ") @@ -103,7 +101,6 @@ func runInit(cmd *cobra.Command, args []string) error { } fmt.Println() - // Step 2: Get instance URL if instanceURL == "" { instanceURL = cfg.InstanceURL } @@ -124,7 +121,6 @@ func runInit(cmd *cobra.Command, args []string) error { } } - // Step 3: Get client ID if clientID == "" { clientID = cfg.ClientID } @@ -144,14 +140,12 @@ func runInit(cmd *cobra.Command, args []string) error { } } - // Step 4: Save configuration cfg.InstanceURL = instanceURL cfg.ClientID = clientID if err := config.Save(cfg); err != nil { return fmt.Errorf("failed to save configuration: %w", err) } - // Step 5: Start OAuth flow oauthConfig := auth.GetOAuthConfig(instanceURL, clientID) authURL := auth.GetAuthURL(oauthConfig) @@ -167,7 +161,6 @@ func runInit(cmd *cobra.Command, args []string) error { fmt.Println(authURL) fmt.Println() - // Start callback server ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() @@ -177,21 +170,18 @@ func runInit(cmd *cobra.Command, args []string) error { fmt.Println("You'll need to manually copy the authorization code.") } - // Open browser (unless --no-browser) if !noBrowser { if err := openBrowser(authURL); err != nil { fmt.Printf("Could not open browser: %v\n", err) } } - // Wait for callback or manual input var code string if resultChan != nil { fmt.Println("Waiting for authorization...") fmt.Println("(Or paste the authorization code or full redirect URL below)") fmt.Println() - // Read from callback or stdin inputChan := make(chan string, 1) go func() { fmt.Print("> ") @@ -222,7 +212,6 @@ func runInit(cmd *cobra.Command, args []string) error { return fmt.Errorf("no authorization code received") } - // Step 6: Exchange code for token fmt.Println() fmt.Println("Exchanging authorization code for tokens...") @@ -231,13 +220,11 @@ func runInit(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to exchange authorization code: %w", err) } - // Step 7: Save token if err := keychain.SetToken(token); err != nil { return fmt.Errorf("failed to save token: %w", err) } fmt.Printf("Token saved to: %s\n", keychain.GetStorageBackend()) - // Step 8: Verify connectivity if !noVerify { fmt.Println() if err := verifyConnectivity(instanceURL); err != nil { @@ -255,7 +242,6 @@ func runInit(cmd *cobra.Command, args []string) error { func extractAuthCode(input string) string { input = strings.TrimSpace(input) - // If it looks like a URL, try to extract the code parameter if strings.HasPrefix(input, "http://localhost") || strings.HasPrefix(input, "https://localhost") { if u, err := url.Parse(input); err == nil { return u.Query().Get("code") @@ -263,7 +249,6 @@ func extractAuthCode(input string) string { return "" } - // Otherwise treat as raw code return input } @@ -279,7 +264,6 @@ func verifyConnectivity(instanceURL string) error { } fmt.Println(" OAuth token: OK") - // Test API access by fetching API versions normalizedURL := "https://" + strings.TrimPrefix(strings.TrimPrefix(instanceURL, "https://"), "http://") resp, err := client.Get(normalizedURL + "/services/data/") if err != nil { diff --git a/internal/cmd/querycmd/query.go b/internal/cmd/querycmd/query.go index e562b32..8d86532 100644 --- a/internal/cmd/querycmd/query.go +++ b/internal/cmd/querycmd/query.go @@ -56,13 +56,10 @@ func runQuery(ctx context.Context, opts *root.Options, soql string, all, noLimit var result *api.QueryResult if all { - // Use queryAll to include deleted/archived records result, err = queryAllRecords(ctx, client, soql) } else if noLimit { - // Fetch all pages result, err = client.QueryAll(ctx, soql) } else { - // Single page query result, err = client.Query(ctx, soql) } @@ -73,13 +70,10 @@ func runQuery(ctx context.Context, opts *root.Options, soql string, all, noLimit return renderQueryResult(opts, result) } -// queryAllRecords uses the queryAll endpoint to include deleted/archived records +// queryAllRecords uses the /queryAll endpoint to include deleted/archived records. func queryAllRecords(ctx context.Context, client *api.Client, soql string) (*api.QueryResult, error) { - // The queryAll endpoint is at /queryAll instead of /query - // We need to make a direct request since the client doesn't have this method path := fmt.Sprintf("/queryAll?q=%s", url.QueryEscape(soql)) - // Use the client's Get method with URL encoding handled body, err := client.Get(ctx, path) if err != nil { return nil, err @@ -101,21 +95,17 @@ func renderQueryResult(opts *root.Options, result *api.QueryResult) error { return nil } - // For JSON output, render the full result if opts.Output == "json" { return v.JSON(result) } - // For table/plain output, extract field names from first record headers := extractHeaders(result.Records) rows := extractRows(result.Records, headers) - // Add record count footer if err := v.Table(headers, rows); err != nil { return err } - // Show pagination info if not all records fetched if !result.Done { v.Info("\nShowing %d of %d records (use --no-limit to fetch all)", len(result.Records), result.TotalSize) } else { @@ -133,7 +123,6 @@ func extractHeaders(records []api.SObject) []string { headers := []string{"Id"} - // Get field names from first record and sort for consistency first := records[0] fieldNames := make([]string, 0, len(first.Fields)) for name := range first.Fields { @@ -175,7 +164,6 @@ func formatFieldValue(v interface{}) string { case string: return val case float64: - // Check if it's a whole number if val == float64(int64(val)) { return fmt.Sprintf("%.0f", val) } @@ -186,7 +174,6 @@ func formatFieldValue(v interface{}) string { } return "false" case map[string]interface{}: - // Nested object (e.g., relationship) if name, ok := val["Name"].(string); ok { return name }