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
94 changes: 94 additions & 0 deletions e2e/testscripts/compositions/compositions.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
env COMP_ID=test-comp-e2e
env INDEX_NAME=test-comp-source-index

# Add a record to the source index so the composition has something to search
exec algolia records import ${INDEX_NAME} --file record.json --wait
! stderr .

# Defer index cleanup
defer algolia index delete ${INDEX_NAME} --confirm
! stderr .

# Upsert a composition using the real behavior.injection schema
exec algolia compositions upsert ${COMP_ID} --file comp.json
! stderr .
stdout '"taskID"'

# Defer composition cleanup
defer algolia compositions delete ${COMP_ID} --confirm
! stderr .

# Get the composition back
exec algolia compositions get ${COMP_ID}
! stderr .
stdout ${COMP_ID}

# List compositions (should contain our composition)
exec algolia compositions list
! stderr .
stdout ${COMP_ID}

# Search the composition
exec algolia compositions search ${COMP_ID} "test"
! stderr .
stdout '"hits"'

# Upsert a rule
exec algolia compositions rules upsert ${COMP_ID} rule-e2e --file rule.json
! stderr .
stdout '"taskID"'

# Get the rule back
exec algolia compositions rules get ${COMP_ID} rule-e2e
! stderr .
stdout 'rule-e2e'

# List rules
exec algolia compositions rules list ${COMP_ID}
! stderr .
stdout 'rule-e2e'

# Delete the rule
exec algolia compositions rules delete ${COMP_ID} rule-e2e --confirm
! stderr .

-- record.json --
{"objectID": "test-record-1", "name": "Test record"}

-- comp.json --
{
"objectID": "test-comp-e2e",
"name": "Test Composition E2E",
"behavior": {
"injection": {
"main": {
"source": {
"search": {
"index": "test-comp-source-index"
}
}
}
}
}
}

-- rule.json --
{
"objectID": "rule-e2e",
"conditions": [
{ "anchoring": "is", "pattern": "test" }
],
"consequence": {
"behavior": {
"injection": {
"main": {
"source": {
"search": {
"index": "test-comp-source-index"
}
}
}
}
}
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/BurntSushi/toml v1.4.0
github.com/MakeNowJust/heredoc v1.0.0
github.com/algolia/algoliasearch-client-go/v4 v4.35.0
github.com/algolia/algoliasearch-client-go/v4 v4.38.0
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869
github.com/briandowns/spinner v1.23.2
github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/algolia/algoliasearch-client-go/v4 v4.35.0 h1:CfckAd2ok/c7/U7MoY3HLW8FeMeAYVBAUFty/d/rxss=
github.com/algolia/algoliasearch-client-go/v4 v4.35.0/go.mod h1:2bHeze2/5+jvT8IYVq8j2NDLr/4R6erGxgud7ESuXww=
github.com/algolia/algoliasearch-client-go/v4 v4.37.2 h1:Iqwb/mx9mVKKWKo8qildTAK36RTLIhFNuufqkhBmiHM=
github.com/algolia/algoliasearch-client-go/v4 v4.37.2/go.mod h1:2bHeze2/5+jvT8IYVq8j2NDLr/4R6erGxgud7ESuXww=
github.com/algolia/algoliasearch-client-go/v4 v4.38.0 h1:ufPpfevxC7DN9vTt9niVc7MvV6inrq8zHpIiTvp2lXI=
github.com/algolia/algoliasearch-client-go/v4 v4.38.0/go.mod h1:2bHeze2/5+jvT8IYVq8j2NDLr/4R6erGxgud7ESuXww=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
Expand Down
30 changes: 30 additions & 0 deletions pkg/cmd/compositions/compositions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package compositions

import (
"github.com/spf13/cobra"

"github.com/algolia/cli/pkg/cmd/compositions/delete"
"github.com/algolia/cli/pkg/cmd/compositions/get"
"github.com/algolia/cli/pkg/cmd/compositions/list"
"github.com/algolia/cli/pkg/cmd/compositions/rules"
compsearch "github.com/algolia/cli/pkg/cmd/compositions/search"
"github.com/algolia/cli/pkg/cmd/compositions/upsert"
"github.com/algolia/cli/pkg/cmdutil"
)

// NewCompositionsCmd returns the compositions command group.
func NewCompositionsCmd(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "compositions",
Short: "Manage Algolia Compositions",
Long: "Create, retrieve, update, delete, and search Algolia Compositions.",
}

cmd.AddCommand(list.NewListCmd(f))
cmd.AddCommand(get.NewGetCmd(f))
cmd.AddCommand(upsert.NewUpsertCmd(f))
cmd.AddCommand(delete.NewDeleteCmd(f))
cmd.AddCommand(compsearch.NewSearchCmd(f))
cmd.AddCommand(rules.NewRulesCmd(f))
return cmd
}
106 changes: 106 additions & 0 deletions pkg/cmd/compositions/delete/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package delete

import (
"fmt"

"github.com/MakeNowJust/heredoc"
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/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)
CompositionID string
DoConfirm bool
PrintFlags *cmdutil.PrintFlags
}

// NewDeleteCmd returns the `compositions delete` command.
func NewDeleteCmd(f *cmdutil.Factory) *cobra.Command {
opts := &DeleteOptions{
IO: f.IOStreams,
Config: f.Config,
CompositionClient: f.CompositionClient,
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."),
Annotations: map[string]string{
"acls": "editSettings",
},
Example: heredoc.Doc(`
# Delete a composition (with confirmation prompt)
$ algolia compositions delete my-comp

# Delete without confirmation
$ algolia compositions delete my-comp --confirm
`),
RunE: func(cmd *cobra.Command, args []string) error {
opts.CompositionID = args[0]

if !opts.DoConfirm {
var confirmed bool
err := prompt.Confirm(
fmt.Sprintf("Delete composition %q?", opts.CompositionID),
&confirmed,
)
if err != nil {
return fmt.Errorf("failed to prompt: %w", err)
}
if !confirmed {
return cmdutil.ErrCancel
}
}

return runDeleteCmd(opts)
},
}

cmd.Flags().BoolVarP(&opts.DoConfirm, "confirm", "y", false, "Skip confirmation prompt")

opts.PrintFlags.AddFlags(cmd)
return cmd
}

func runDeleteCmd(opts *DeleteOptions) error {
client, err := opts.CompositionClient()
if err != nil {
return err
}

p, err := opts.PrintFlags.ToPrinter()
if err != nil {
return err
}

opts.IO.StartProgressIndicatorWithLabel("Deleting composition")

res, err := client.DeleteComposition(
client.NewApiDeleteCompositionRequest(opts.CompositionID),
)
if err != nil {
opts.IO.StopProgressIndicator()
return err
}

opts.IO.StopProgressIndicator()

if err := compinternal.WaitForTask(opts.IO, client, opts.CompositionID, res.TaskID, compinternal.PollInterval, compinternal.Timeout); err != nil {
return err
}

return p.Print(opts.IO, res)
}
88 changes: 88 additions & 0 deletions pkg/cmd/compositions/delete/delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package delete_test

import (
"strings"
"testing"
"time"

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

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

func TestDeleteComposition(t *testing.T) {
r := &httpmock.Registry{}
r.Register(httpmock.REST("DELETE", "1/compositions/my-comp"), httpmock.StringResponse(`{"taskID":99}`))
r.Register(httpmock.REST("GET", "1/compositions/my-comp/task/99"), httpmock.StringResponse(`{"status":"published"}`))

compinternal.PollInterval = 1 * time.Millisecond
compinternal.Timeout = 50 * time.Millisecond
t.Cleanup(func() {
compinternal.PollInterval = compinternal.DefaultPollInterval
compinternal.Timeout = compinternal.DefaultTimeout
})

f, out := test.NewFactory(false, r, nil, "")
cmd := delete.NewDeleteCmd(f)
_, err := test.Execute(cmd, "my-comp --confirm", out)
require.NoError(t, err)

assert.JSONEq(t, `{"taskID":99}`, strings.TrimSpace(out.String()))
r.Verify(t)
}

func TestDeleteComposition_WaitsForPublished(t *testing.T) {
// Verifies polling continues through successive notPublished states.
r := &httpmock.Registry{}
r.Register(httpmock.REST("DELETE", "1/compositions/my-comp"), httpmock.StringResponse(`{"taskID":77}`))
r.Register(httpmock.REST("GET", "1/compositions/my-comp/task/77"), httpmock.StringResponse(`{"status":"notPublished"}`))
r.Register(httpmock.REST("GET", "1/compositions/my-comp/task/77"), httpmock.StringResponse(`{"status":"notPublished"}`))
r.Register(httpmock.REST("GET", "1/compositions/my-comp/task/77"), httpmock.StringResponse(`{"status":"published"}`))

compinternal.PollInterval = 1 * time.Millisecond
compinternal.Timeout = 50 * time.Millisecond
t.Cleanup(func() {
compinternal.PollInterval = compinternal.DefaultPollInterval
compinternal.Timeout = compinternal.DefaultTimeout
})

f, out := test.NewFactory(false, r, nil, "")
cmd := delete.NewDeleteCmd(f)
_, err := test.Execute(cmd, "my-comp --confirm", out)
require.NoError(t, err)

assert.JSONEq(t, `{"taskID":77}`, strings.TrimSpace(out.String()))
r.Verify(t)

taskPolls := 0
for _, req := range r.Requests {
if strings.Contains(req.URL.Path, "/task/") {
taskPolls++
}
}
assert.Equal(t, 3, taskPolls, "expected 3 task status polls (2x notPublished + 1x published)")
}

func TestDeleteComposition_RequiresConfirmation(t *testing.T) {
// Without --confirm on a non-TTY, the prompt must fail and no HTTP request must be made.
r := &httpmock.Registry{}
f, out := test.NewFactory(false, r, nil, "")
cmd := delete.NewDeleteCmd(f)
_, err := test.Execute(cmd, "my-comp", out)
require.Error(t, err)
assert.Empty(t, out.String())
assert.Empty(t, r.Requests)
}

func TestDeleteComposition_MissingArg(t *testing.T) {
r := &httpmock.Registry{}
f, out := test.NewFactory(false, r, nil, "")
cmd := delete.NewDeleteCmd(f)
_, err := test.Execute(cmd, "", out)
require.Error(t, err)
assert.Contains(t, err.Error(), "requires a <composition-id> argument")
}
Loading
Loading