diff --git a/api/graphql.go b/api/graphql.go index 5568ba6..ece5c22 100644 --- a/api/graphql.go +++ b/api/graphql.go @@ -3,6 +3,7 @@ package api import ( "encoding/json" "fmt" + "strings" ) // GraphQLRequest represents a GraphQL query request @@ -46,17 +47,11 @@ func (r *GraphQLResponse) ErrorMessages() string { if !r.HasErrors() { return "" } - if len(r.Errors) == 1 { - return r.Errors[0].Message - } - msg := "" + messages := make([]string, len(r.Errors)) for i, e := range r.Errors { - if i > 0 { - msg += "; " - } - msg += e.Message + messages[i] = e.Message } - return msg + return strings.Join(messages, "; ") } // ExecuteGraphQL executes a GraphQL query @@ -290,7 +285,7 @@ func (s *IntrospectionSchema) GetRootTypes() []IntrospectionType { var result []IntrospectionType for _, t := range s.Types { // Skip internal types (start with __) - if len(t.Name) > 0 && t.Name[0:1] != "_" { + if !strings.HasPrefix(t.Name, "__") { // Include OBJECT and INTERFACE types that have fields if (t.Kind == "OBJECT" || t.Kind == "INTERFACE") && len(t.Fields) > 0 { result = append(result, t) diff --git a/internal/cmd/graphql/graphql.go b/internal/cmd/graphql/graphql.go index 2ce1ed4..f04e67d 100644 --- a/internal/cmd/graphql/graphql.go +++ b/internal/cmd/graphql/graphql.go @@ -11,6 +11,13 @@ import ( "github.com/open-cli-collective/hubspot-cli/internal/cmd/root" ) +// tableRenderer is the interface for rendering tabular output +type tableRenderer interface { + Render([]string, [][]string, interface{}) error + Info(string, ...interface{}) + Error(string, ...interface{}) +} + // Register registers the graphql command and subcommands func Register(parent *cobra.Command, opts *root.Options) { cmd := &cobra.Command{ @@ -185,10 +192,7 @@ specific type, and --field to show details of a specific field.`, return cmd } -func listRootTypes(v interface { - Render([]string, [][]string, interface{}) error - Info(string, ...interface{}) -}, schema *api.IntrospectionSchema) error { +func listRootTypes(v tableRenderer, schema *api.IntrospectionSchema) error { types := schema.GetRootTypes() if len(types) == 0 { v.Info("No types found in schema") @@ -214,10 +218,7 @@ func listRootTypes(v interface { return v.Render(headers, rows, types) } -func showTypeFields(v interface { - Render([]string, [][]string, interface{}) error - Info(string, ...interface{}) -}, t *api.IntrospectionType) error { +func showTypeFields(v tableRenderer, t *api.IntrospectionType) error { if len(t.Fields) == 0 { v.Info("Type %s has no fields", t.Name) return nil @@ -246,11 +247,7 @@ func showTypeFields(v interface { return v.Render(headers, rows, t.Fields) } -func showFieldDetails(v interface { - Render([]string, [][]string, interface{}) error - Info(string, ...interface{}) - Error(string, ...interface{}) -}, t *api.IntrospectionType, fieldName string) error { +func showFieldDetails(v tableRenderer, t *api.IntrospectionType, fieldName string) error { var field *api.IntrospectionField for i := range t.Fields { if t.Fields[i].Name == fieldName { diff --git a/internal/cmd/schemas/schemas.go b/internal/cmd/schemas/schemas.go index ea0c826..af7dbc4 100644 --- a/internal/cmd/schemas/schemas.go +++ b/internal/cmd/schemas/schemas.go @@ -10,6 +10,7 @@ import ( "github.com/open-cli-collective/hubspot-cli/api" "github.com/open-cli-collective/hubspot-cli/internal/cmd/root" + "github.com/open-cli-collective/hubspot-cli/internal/cmd/shared" ) // Register registers the schemas command and subcommands @@ -129,7 +130,7 @@ func newGetCmd(opts *root.Options) *cobra.Command { {"Singular Label", schema.Labels.Singular}, {"Plural Label", schema.Labels.Plural}, {"Primary Display Property", schema.PrimaryDisplayProperty}, - {"Archived", formatBool(schema.Archived)}, + {"Archived", shared.FormatBool(schema.Archived)}, {"Created", schema.CreatedAt}, {"Updated", schema.UpdatedAt}, } @@ -198,17 +199,27 @@ func newCreateCmd(opts *root.Options) *cobra.Command { } func newDeleteCmd(opts *root.Options) *cobra.Command { - return &cobra.Command{ + var force bool + + cmd := &cobra.Command{ Use: "delete ", Short: "Delete a custom object schema", Long: "Delete a custom object schema by its fully qualified name. This will also delete all objects of that type.", Example: ` # Delete schema by fully qualified name - hspt schemas delete p_my_custom_object`, + hspt schemas delete p_my_custom_object + + # Delete without confirmation + hspt schemas delete p_my_custom_object --force`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { v := opts.View() fqn := args[0] + if !force { + v.Warning("This will delete schema %s and all objects of that type. Use --force to confirm.", fqn) + return nil + } + client, err := opts.APIClient() if err != nil { return err @@ -227,11 +238,8 @@ func newDeleteCmd(opts *root.Options) *cobra.Command { return nil }, } -} -func formatBool(b bool) string { - if b { - return "Yes" - } - return "No" + cmd.Flags().BoolVar(&force, "force", false, "Confirm deletion without prompt") + + return cmd } diff --git a/internal/cmd/shared/format.go b/internal/cmd/shared/format.go new file mode 100644 index 0000000..5a4313b --- /dev/null +++ b/internal/cmd/shared/format.go @@ -0,0 +1,10 @@ +package shared + +// FormatBool returns "Yes" or "No" for boolean values. +// Used for human-readable output in table views. +func FormatBool(b bool) string { + if b { + return "Yes" + } + return "No" +} diff --git a/internal/cmd/shared/format_test.go b/internal/cmd/shared/format_test.go new file mode 100644 index 0000000..c3fb0e4 --- /dev/null +++ b/internal/cmd/shared/format_test.go @@ -0,0 +1,23 @@ +package shared + +import "testing" + +func TestFormatBool(t *testing.T) { + tests := []struct { + name string + input bool + want string + }{ + {"true returns Yes", true, "Yes"}, + {"false returns No", false, "No"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := FormatBool(tt.input) + if got != tt.want { + t.Errorf("FormatBool(%v) = %q, want %q", tt.input, got, tt.want) + } + }) + } +} diff --git a/internal/cmd/workflows/workflows.go b/internal/cmd/workflows/workflows.go index 3cc04e6..1869337 100644 --- a/internal/cmd/workflows/workflows.go +++ b/internal/cmd/workflows/workflows.go @@ -9,6 +9,7 @@ import ( "github.com/open-cli-collective/hubspot-cli/api" "github.com/open-cli-collective/hubspot-cli/internal/cmd/root" + "github.com/open-cli-collective/hubspot-cli/internal/cmd/shared" ) // Register registers the workflows command and subcommands @@ -71,7 +72,7 @@ func newListCmd(opts *root.Options) *cobra.Command { workflow.ID, workflow.Name, workflow.Type, - formatBool(workflow.Enabled), + shared.FormatBool(workflow.Enabled), formatObjectType(workflow.ObjectTypeID), }) } @@ -125,7 +126,7 @@ func newGetCmd(opts *root.Options) *cobra.Command { {"ID", workflow.ID}, {"Name", workflow.Name}, {"Type", workflow.Type}, - {"Enabled", formatBool(workflow.Enabled)}, + {"Enabled", shared.FormatBool(workflow.Enabled)}, {"Object Type", formatObjectType(workflow.ObjectTypeID)}, {"Revision ID", workflow.RevisionID}, {"Created", workflow.CreatedAt}, @@ -137,13 +138,6 @@ func newGetCmd(opts *root.Options) *cobra.Command { } } -func formatBool(b bool) string { - if b { - return "Yes" - } - return "No" -} - func formatObjectType(objectTypeID string) string { // HubSpot standard object type IDs objectTypes := map[string]string{ @@ -264,17 +258,27 @@ func newUpdateCmd(opts *root.Options) *cobra.Command { } func newDeleteCmd(opts *root.Options) *cobra.Command { - return &cobra.Command{ + var force bool + + cmd := &cobra.Command{ Use: "delete ", Short: "Delete a workflow", Long: "Delete an automation workflow by ID.", Example: ` # Delete a workflow - hspt workflows delete 12345`, + hspt workflows delete 12345 + + # Delete without confirmation + hspt workflows delete 12345 --force`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { v := opts.View() id := args[0] + if !force { + v.Warning("This will delete workflow %s. Use --force to confirm.", id) + return nil + } + client, err := opts.APIClient() if err != nil { return err @@ -293,6 +297,10 @@ func newDeleteCmd(opts *root.Options) *cobra.Command { return nil }, } + + cmd.Flags().BoolVar(&force, "force", false, "Confirm deletion without prompt") + + return cmd } func newEnrollCmd(opts *root.Options) *cobra.Command {