Skip to content
Draft
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
3 changes: 2 additions & 1 deletion pkg/cmd/compositions/compositions.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ func NewCompositionsCmd(f *cmdutil.Factory) *cobra.Command {

cmd.AddCommand(list.NewListCmd(f))
cmd.AddCommand(get.NewGetCmd(f))
cmd.AddCommand(upsert.NewUpsertCmd(f))
cmd.AddCommand(upsert.NewInsertCmd(f))
cmd.AddCommand(upsert.NewUpdateCmd(f))
cmd.AddCommand(sortingstrategy.NewSortingStrategyCmd(f))
cmd.AddCommand(delete.NewDeleteCmd(f))
cmd.AddCommand(compsearch.NewSearchCmd(f))
Expand Down
6 changes: 3 additions & 3 deletions pkg/cmd/compositions/compositions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ const wantBody = `{
}
}`

// Drives `compositions upsert --interactive` through the real command tree with
// Drives `compositions insert --interactive` through the real command tree with
// a label-keyed ScriptedPrompter on the Factory. Answers are keyed by a unique
// substring of the prompt label, so adding or reordering SDK fields does not
// break the INPUT side: unmatched prompts fall back to safe defaults (skip).
func TestCompositions_UpsertInteractive(t *testing.T) {
func TestCompositions_InsertInteractive(t *testing.T) {
r := &httpmock.Registry{}
var captured []byte
r.Register(httpmock.REST("PUT", "1/compositions/my-comp"), func(req *http.Request) (*http.Response, error) {
Expand Down Expand Up @@ -77,7 +77,7 @@ func TestCompositions_UpsertInteractive(t *testing.T) {
}

cmd := compositions.NewCompositionsCmd(f)
_, err := test.Execute(cmd, "upsert my-comp --interactive", out)
_, err := test.Execute(cmd, "insert my-comp --interactive", out)
require.NoError(t, err)

assert.JSONEq(t, wantBody, string(captured))
Expand Down
17 changes: 12 additions & 5 deletions pkg/cmd/compositions/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ import (
compinternal "github.com/algolia/cli/pkg/cmd/compositions/internal"
"github.com/algolia/cli/pkg/cmdutil"
"github.com/algolia/cli/pkg/config"
"github.com/algolia/cli/pkg/interactive"
"github.com/algolia/cli/pkg/iostreams"
"github.com/algolia/cli/pkg/prompt"
"github.com/algolia/cli/pkg/validators"
)

// DeleteOptions holds the dependencies and flags for the delete command.
type DeleteOptions struct {
Config config.IConfig
IO *iostreams.IOStreams
CompositionClient func() (*algoliaComposition.APIClient, error)
Prompter interactive.Prompter
CompositionID string
DoConfirm bool
PrintFlags *cmdutil.PrintFlags
Expand All @@ -31,13 +32,15 @@ func NewDeleteCmd(f *cmdutil.Factory) *cobra.Command {
IO: f.IOStreams,
Config: f.Config,
CompositionClient: f.CompositionClient,
Prompter: f.Prompter,
PrintFlags: cmdutil.NewPrintFlags().WithDefaultOutput("json"),
}

cmd := &cobra.Command{
Use: "delete <composition-id>",
Short: "Delete a composition",
Args: validators.ExactArgsWithMsg(1, "compositions delete requires a <composition-id> argument."),
Use: "delete [composition-id]",
Short: "Delete a composition",
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: cmdutil.CompositionIDs(opts.CompositionClient),
Annotations: map[string]string{
"acls": "editSettings",
},
Expand All @@ -49,7 +52,11 @@ func NewDeleteCmd(f *cmdutil.Factory) *cobra.Command {
$ algolia compositions delete my-comp --confirm
`),
RunE: func(cmd *cobra.Command, args []string) error {
opts.CompositionID = args[0]
id, err := compinternal.ResolveCompositionID(args, opts.IO.CanPrompt(), opts.Prompter, opts.CompositionClient)
if err != nil {
return err
}
opts.CompositionID = id

if !opts.DoConfirm {
if !opts.IO.CanPrompt() {
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/compositions/delete/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,5 @@ func TestDeleteComposition_MissingArg(t *testing.T) {
cmd := delete.NewDeleteCmd(f)
_, err := test.Execute(cmd, "", out)
require.Error(t, err)
assert.Contains(t, err.Error(), "requires a <composition-id> argument")
assert.Contains(t, err.Error(), "a <composition-id> argument is required")
}
21 changes: 16 additions & 5 deletions pkg/cmd/compositions/get/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@ import (
algoliaComposition "github.com/algolia/algoliasearch-client-go/v4/algolia/composition"
"github.com/spf13/cobra"

compinternal "github.com/algolia/cli/pkg/cmd/compositions/internal"
"github.com/algolia/cli/pkg/cmdutil"
"github.com/algolia/cli/pkg/config"
"github.com/algolia/cli/pkg/interactive"
"github.com/algolia/cli/pkg/iostreams"
"github.com/algolia/cli/pkg/validators"
)

// GetOptions holds the dependencies and flags for the get command.
type GetOptions struct {
Config config.IConfig
IO *iostreams.IOStreams
CompositionClient func() (*algoliaComposition.APIClient, error)
Prompter interactive.Prompter
CompositionID string
PrintFlags *cmdutil.PrintFlags
}
Expand All @@ -26,22 +28,31 @@ func NewGetCmd(f *cmdutil.Factory) *cobra.Command {
IO: f.IOStreams,
Config: f.Config,
CompositionClient: f.CompositionClient,
Prompter: f.Prompter,
PrintFlags: cmdutil.NewPrintFlags().WithDefaultOutput("json"),
}

cmd := &cobra.Command{
Use: "get <composition-id>",
Short: "Get a composition by ID",
Args: validators.ExactArgsWithMsg(1, "compositions get requires a <composition-id> argument."),
Use: "get [composition-id]",
Short: "Get a composition by ID",
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: cmdutil.CompositionIDs(opts.CompositionClient),
Annotations: map[string]string{
"acls": "search",
},
Example: heredoc.Doc(`
# Get the composition with ID "my-comp"
$ algolia compositions get my-comp

# Pick a composition to get from a list
$ algolia compositions get
`),
RunE: func(cmd *cobra.Command, args []string) error {
opts.CompositionID = args[0]
id, err := compinternal.ResolveCompositionID(args, opts.IO.CanPrompt(), opts.Prompter, opts.CompositionClient)
if err != nil {
return err
}
opts.CompositionID = id
return runGetCmd(opts)
},
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/compositions/get/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,5 @@ func TestGetComposition_MissingArg(t *testing.T) {
cmd := get.NewGetCmd(f)
_, err := test.Execute(cmd, "", out)
require.Error(t, err)
assert.Contains(t, err.Error(), "requires a <composition-id> argument")
assert.Contains(t, err.Error(), "a <composition-id> argument is required")
}
95 changes: 95 additions & 0 deletions pkg/cmd/compositions/internal/select.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package internal

import (
"errors"

"github.com/algolia/algoliasearch-client-go/v4/algolia/composition"

"github.com/algolia/cli/pkg/cmdutil"
"github.com/algolia/cli/pkg/interactive"
)

// ResolveCompositionID returns the id from args, or prompts a selector over
// existing compositions when the argument is omitted on a terminal.
func ResolveCompositionID(
args []string,
canPrompt bool,
prompter interactive.Prompter,
clientF func() (*composition.APIClient, error),
) (string, error) {
if len(args) > 0 {
return args[0], nil
}
if !canPrompt {
return "", cmdutil.FlagErrorf("a <composition-id> argument is required")
}
return SelectCompositionID(prompter, clientF)
}

// ResolveCompositionAndRuleID resolves a composition id and a rule id from args,
// prompting selectors for whichever is omitted on a terminal. The rule list is
// scoped to the resolved composition.
func ResolveCompositionAndRuleID(
args []string,
canPrompt bool,
prompter interactive.Prompter,
clientF func() (*composition.APIClient, error),
) (string, string, error) {
compositionID, err := ResolveCompositionID(args, canPrompt, prompter, clientF)
if err != nil {
return "", "", err
}
if len(args) >= 2 {
return compositionID, args[1], nil
}
if !canPrompt {
return "", "", cmdutil.FlagErrorf("a <rule-id> argument is required")
}
ruleID, err := SelectRuleID(prompter, clientF, compositionID)
if err != nil {
return "", "", err
}
return compositionID, ruleID, nil
}

// SelectRuleID lists the rules of a composition and prompts the user to pick one.
func SelectRuleID(prompter interactive.Prompter, clientF func() (*composition.APIClient, error), compositionID string) (string, error) {
client, err := clientF()
if err != nil {
return "", err
}
res, err := client.SearchCompositionRules(client.NewApiSearchCompositionRulesRequest(compositionID))
if err != nil {
return "", err
}
if len(res.Hits) == 0 {
return "", errors.New("no rules found for composition " + compositionID)
}
ids := make([]string, len(res.Hits))
for i, hit := range res.Hits {
ids[i] = hit.ObjectID
}
return prompter.Select("Rule", ids)
}

// SelectCompositionID lists existing compositions and prompts the user to pick
// one, returning its object id. Used when a command is called without the
// composition-id argument on a terminal.
func SelectCompositionID(prompter interactive.Prompter, clientF func() (*composition.APIClient, error)) (string, error) {
client, err := clientF()
if err != nil {
return "", err
}
res, err := client.ListCompositions(client.NewApiListCompositionsRequest())
if err != nil {
return "", err
}
if len(res.Items) == 0 {
return "", errors.New("no compositions found")
}
ids := make([]string, len(res.Items))
for i, comp := range res.Items {
ids[i] = comp.ObjectID
}
return prompter.Select("Composition", ids)
}
36 changes: 36 additions & 0 deletions pkg/cmd/compositions/internal/select_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package internal_test

import (
"testing"

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

compinternal "github.com/algolia/cli/pkg/cmd/compositions/internal"
"github.com/algolia/cli/pkg/httpmock"
"github.com/algolia/cli/pkg/interactive"
)

func TestResolveCompositionID_FromArg(t *testing.T) {
// an explicit arg is returned as-is, no client or prompt needed.
id, err := compinternal.ResolveCompositionID([]string{"c1"}, true, nil, nil)
require.NoError(t, err)
assert.Equal(t, "c1", id)
}

func TestResolveCompositionID_NoArgNoTTY(t *testing.T) {
_, err := compinternal.ResolveCompositionID(nil, false, nil, nil)
assert.Error(t, err)
}

func TestResolveCompositionID_Selector(t *testing.T) {
r := &httpmock.Registry{}
r.Register(httpmock.REST("GET", "1/compositions"),
httpmock.StringResponse(`{"items":[{"objectID":"alpha"},{"objectID":"beta"}],"nbPages":1}`))

prompter := &interactive.ScriptedPrompter{Selects: map[string]string{"Composition": "beta"}}

id, err := compinternal.ResolveCompositionID(nil, true, prompter, compositionClientFor(r))
require.NoError(t, err)
assert.Equal(t, "beta", id)
}
Loading
Loading