Skip to content

Commit 29a9b64

Browse files
ben-kalmusclaude
andauthored
feat: add CompositionClient to cli and bump SDK to v4.38.0 (#215)
* feat: add CompositionClient to cli and bump SDK to v4.38.0 * fix(compositions): apply PR review feedback - Register TestCompositions in e2e_test.go so the txtar script runs - Guard interactive prompts with CanPrompt() in both delete commands - Return nil (not ErrCancel) when user declines confirmation, matching repo convention - Wrap prompt error in rules/delete to match sibling style - Print TTY success message after WaitForTask in all four mutate commands - Issue an immediate GetTask in WaitForTask so quick tasks skip the first poll wait Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 177f309 commit 29a9b64

30 files changed

Lines changed: 1913 additions & 7 deletions

e2e/e2e_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,8 @@ func TestSearch(t *testing.T) {
153153
func TestAgentReady(t *testing.T) {
154154
runTestsInDir(t, "testscripts/agent-ready")
155155
}
156+
157+
// TestCompositions tests `algolia compositions` commands
158+
func TestCompositions(t *testing.T) {
159+
runTestsInDir(t, "testscripts/compositions")
160+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
env COMP_ID=test-comp-e2e
2+
env INDEX_NAME=test-comp-source-index
3+
4+
# Add a record to the source index so the composition has something to search
5+
exec algolia records import ${INDEX_NAME} --file record.json --wait
6+
! stderr .
7+
8+
# Defer index cleanup
9+
defer algolia index delete ${INDEX_NAME} --confirm
10+
! stderr .
11+
12+
# Upsert a composition using the real behavior.injection schema
13+
exec algolia compositions upsert ${COMP_ID} --file comp.json
14+
! stderr .
15+
stdout '"taskID"'
16+
17+
# Defer composition cleanup
18+
defer algolia compositions delete ${COMP_ID} --confirm
19+
! stderr .
20+
21+
# Get the composition back
22+
exec algolia compositions get ${COMP_ID}
23+
! stderr .
24+
stdout ${COMP_ID}
25+
26+
# List compositions (should contain our composition)
27+
exec algolia compositions list
28+
! stderr .
29+
stdout ${COMP_ID}
30+
31+
# Search the composition
32+
exec algolia compositions search ${COMP_ID} "test"
33+
! stderr .
34+
stdout '"hits"'
35+
36+
# Upsert a rule
37+
exec algolia compositions rules upsert ${COMP_ID} rule-e2e --file rule.json
38+
! stderr .
39+
stdout '"taskID"'
40+
41+
# Get the rule back
42+
exec algolia compositions rules get ${COMP_ID} rule-e2e
43+
! stderr .
44+
stdout 'rule-e2e'
45+
46+
# List rules
47+
exec algolia compositions rules list ${COMP_ID}
48+
! stderr .
49+
stdout 'rule-e2e'
50+
51+
# Delete the rule
52+
exec algolia compositions rules delete ${COMP_ID} rule-e2e --confirm
53+
! stderr .
54+
55+
-- record.json --
56+
{"objectID": "test-record-1", "name": "Test record"}
57+
58+
-- comp.json --
59+
{
60+
"objectID": "test-comp-e2e",
61+
"name": "Test Composition E2E",
62+
"behavior": {
63+
"injection": {
64+
"main": {
65+
"source": {
66+
"search": {
67+
"index": "test-comp-source-index"
68+
}
69+
}
70+
}
71+
}
72+
}
73+
}
74+
75+
-- rule.json --
76+
{
77+
"objectID": "rule-e2e",
78+
"conditions": [
79+
{ "anchoring": "is", "pattern": "test" }
80+
],
81+
"consequence": {
82+
"behavior": {
83+
"injection": {
84+
"main": {
85+
"source": {
86+
"search": {
87+
"index": "test-comp-source-index"
88+
}
89+
}
90+
}
91+
}
92+
}
93+
}
94+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ require (
88
github.com/AlecAivazis/survey/v2 v2.3.7
99
github.com/BurntSushi/toml v1.4.0
1010
github.com/MakeNowJust/heredoc v1.0.0
11-
github.com/algolia/algoliasearch-client-go/v4 v4.35.0
11+
github.com/algolia/algoliasearch-client-go/v4 v4.38.0
1212
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869
1313
github.com/briandowns/spinner v1.23.2
1414
github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
88
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
99
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
1010
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
11-
github.com/algolia/algoliasearch-client-go/v4 v4.35.0 h1:CfckAd2ok/c7/U7MoY3HLW8FeMeAYVBAUFty/d/rxss=
12-
github.com/algolia/algoliasearch-client-go/v4 v4.35.0/go.mod h1:2bHeze2/5+jvT8IYVq8j2NDLr/4R6erGxgud7ESuXww=
11+
github.com/algolia/algoliasearch-client-go/v4 v4.37.2 h1:Iqwb/mx9mVKKWKo8qildTAK36RTLIhFNuufqkhBmiHM=
12+
github.com/algolia/algoliasearch-client-go/v4 v4.37.2/go.mod h1:2bHeze2/5+jvT8IYVq8j2NDLr/4R6erGxgud7ESuXww=
13+
github.com/algolia/algoliasearch-client-go/v4 v4.38.0 h1:ufPpfevxC7DN9vTt9niVc7MvV6inrq8zHpIiTvp2lXI=
14+
github.com/algolia/algoliasearch-client-go/v4 v4.38.0/go.mod h1:2bHeze2/5+jvT8IYVq8j2NDLr/4R6erGxgud7ESuXww=
1315
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
1416
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
1517
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package compositions
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/algolia/cli/pkg/cmd/compositions/delete"
7+
"github.com/algolia/cli/pkg/cmd/compositions/get"
8+
"github.com/algolia/cli/pkg/cmd/compositions/list"
9+
"github.com/algolia/cli/pkg/cmd/compositions/rules"
10+
compsearch "github.com/algolia/cli/pkg/cmd/compositions/search"
11+
"github.com/algolia/cli/pkg/cmd/compositions/upsert"
12+
"github.com/algolia/cli/pkg/cmdutil"
13+
)
14+
15+
// NewCompositionsCmd returns the compositions command group.
16+
func NewCompositionsCmd(f *cmdutil.Factory) *cobra.Command {
17+
cmd := &cobra.Command{
18+
Use: "compositions",
19+
Short: "Manage Algolia Compositions",
20+
Long: "Create, retrieve, update, delete, and search Algolia Compositions.",
21+
}
22+
23+
cmd.AddCommand(list.NewListCmd(f))
24+
cmd.AddCommand(get.NewGetCmd(f))
25+
cmd.AddCommand(upsert.NewUpsertCmd(f))
26+
cmd.AddCommand(delete.NewDeleteCmd(f))
27+
cmd.AddCommand(compsearch.NewSearchCmd(f))
28+
cmd.AddCommand(rules.NewRulesCmd(f))
29+
return cmd
30+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package delete
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/MakeNowJust/heredoc"
7+
algoliaComposition "github.com/algolia/algoliasearch-client-go/v4/algolia/composition"
8+
"github.com/spf13/cobra"
9+
10+
compinternal "github.com/algolia/cli/pkg/cmd/compositions/internal"
11+
"github.com/algolia/cli/pkg/cmdutil"
12+
"github.com/algolia/cli/pkg/config"
13+
"github.com/algolia/cli/pkg/iostreams"
14+
"github.com/algolia/cli/pkg/prompt"
15+
"github.com/algolia/cli/pkg/validators"
16+
)
17+
18+
// DeleteOptions holds the dependencies and flags for the delete command.
19+
type DeleteOptions struct {
20+
Config config.IConfig
21+
IO *iostreams.IOStreams
22+
CompositionClient func() (*algoliaComposition.APIClient, error)
23+
CompositionID string
24+
DoConfirm bool
25+
PrintFlags *cmdutil.PrintFlags
26+
}
27+
28+
// NewDeleteCmd returns the `compositions delete` command.
29+
func NewDeleteCmd(f *cmdutil.Factory) *cobra.Command {
30+
opts := &DeleteOptions{
31+
IO: f.IOStreams,
32+
Config: f.Config,
33+
CompositionClient: f.CompositionClient,
34+
PrintFlags: cmdutil.NewPrintFlags().WithDefaultOutput("json"),
35+
}
36+
37+
cmd := &cobra.Command{
38+
Use: "delete <composition-id>",
39+
Short: "Delete a composition",
40+
Args: validators.ExactArgsWithMsg(1, "compositions delete requires a <composition-id> argument."),
41+
Annotations: map[string]string{
42+
"acls": "editSettings",
43+
},
44+
Example: heredoc.Doc(`
45+
# Delete a composition (with confirmation prompt)
46+
$ algolia compositions delete my-comp
47+
48+
# Delete without confirmation
49+
$ algolia compositions delete my-comp --confirm
50+
`),
51+
RunE: func(cmd *cobra.Command, args []string) error {
52+
opts.CompositionID = args[0]
53+
54+
if !opts.DoConfirm {
55+
if !opts.IO.CanPrompt() {
56+
return cmdutil.FlagErrorf("--confirm required when non-interactive shell is detected")
57+
}
58+
var confirmed bool
59+
err := prompt.Confirm(
60+
fmt.Sprintf("Delete composition %q?", opts.CompositionID),
61+
&confirmed,
62+
)
63+
if err != nil {
64+
return fmt.Errorf("failed to prompt: %w", err)
65+
}
66+
if !confirmed {
67+
return nil
68+
}
69+
}
70+
71+
return runDeleteCmd(opts)
72+
},
73+
}
74+
75+
cmd.Flags().BoolVarP(&opts.DoConfirm, "confirm", "y", false, "Skip confirmation prompt")
76+
77+
opts.PrintFlags.AddFlags(cmd)
78+
return cmd
79+
}
80+
81+
func runDeleteCmd(opts *DeleteOptions) error {
82+
client, err := opts.CompositionClient()
83+
if err != nil {
84+
return err
85+
}
86+
87+
p, err := opts.PrintFlags.ToPrinter()
88+
if err != nil {
89+
return err
90+
}
91+
92+
opts.IO.StartProgressIndicatorWithLabel("Deleting composition")
93+
94+
res, err := client.DeleteComposition(
95+
client.NewApiDeleteCompositionRequest(opts.CompositionID),
96+
)
97+
if err != nil {
98+
opts.IO.StopProgressIndicator()
99+
return err
100+
}
101+
102+
opts.IO.StopProgressIndicator()
103+
104+
if err := compinternal.WaitForTask(opts.IO, client, opts.CompositionID, res.TaskID, compinternal.PollInterval, compinternal.Timeout); err != nil {
105+
return err
106+
}
107+
108+
if opts.IO.IsStdoutTTY() {
109+
cs := opts.IO.ColorScheme()
110+
fmt.Fprintf(opts.IO.Out, "%s Deleted composition %s\n", cs.SuccessIcon(), opts.CompositionID)
111+
}
112+
113+
return p.Print(opts.IO, res)
114+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package delete_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/algolia/cli/pkg/cmd/compositions/delete"
12+
compinternal "github.com/algolia/cli/pkg/cmd/compositions/internal"
13+
"github.com/algolia/cli/pkg/httpmock"
14+
"github.com/algolia/cli/test"
15+
)
16+
17+
func TestDeleteComposition(t *testing.T) {
18+
r := &httpmock.Registry{}
19+
r.Register(httpmock.REST("DELETE", "1/compositions/my-comp"), httpmock.StringResponse(`{"taskID":99}`))
20+
r.Register(httpmock.REST("GET", "1/compositions/my-comp/task/99"), httpmock.StringResponse(`{"status":"published"}`))
21+
22+
compinternal.PollInterval = 1 * time.Millisecond
23+
compinternal.Timeout = 50 * time.Millisecond
24+
t.Cleanup(func() {
25+
compinternal.PollInterval = compinternal.DefaultPollInterval
26+
compinternal.Timeout = compinternal.DefaultTimeout
27+
})
28+
29+
f, out := test.NewFactory(false, r, nil, "")
30+
cmd := delete.NewDeleteCmd(f)
31+
_, err := test.Execute(cmd, "my-comp --confirm", out)
32+
require.NoError(t, err)
33+
34+
assert.JSONEq(t, `{"taskID":99}`, strings.TrimSpace(out.String()))
35+
r.Verify(t)
36+
}
37+
38+
func TestDeleteComposition_WaitsForPublished(t *testing.T) {
39+
// Verifies polling continues through successive notPublished states.
40+
r := &httpmock.Registry{}
41+
r.Register(httpmock.REST("DELETE", "1/compositions/my-comp"), httpmock.StringResponse(`{"taskID":77}`))
42+
r.Register(httpmock.REST("GET", "1/compositions/my-comp/task/77"), httpmock.StringResponse(`{"status":"notPublished"}`))
43+
r.Register(httpmock.REST("GET", "1/compositions/my-comp/task/77"), httpmock.StringResponse(`{"status":"notPublished"}`))
44+
r.Register(httpmock.REST("GET", "1/compositions/my-comp/task/77"), httpmock.StringResponse(`{"status":"published"}`))
45+
46+
compinternal.PollInterval = 1 * time.Millisecond
47+
compinternal.Timeout = 50 * time.Millisecond
48+
t.Cleanup(func() {
49+
compinternal.PollInterval = compinternal.DefaultPollInterval
50+
compinternal.Timeout = compinternal.DefaultTimeout
51+
})
52+
53+
f, out := test.NewFactory(false, r, nil, "")
54+
cmd := delete.NewDeleteCmd(f)
55+
_, err := test.Execute(cmd, "my-comp --confirm", out)
56+
require.NoError(t, err)
57+
58+
assert.JSONEq(t, `{"taskID":77}`, strings.TrimSpace(out.String()))
59+
r.Verify(t)
60+
61+
taskPolls := 0
62+
for _, req := range r.Requests {
63+
if strings.Contains(req.URL.Path, "/task/") {
64+
taskPolls++
65+
}
66+
}
67+
assert.Equal(t, 3, taskPolls, "expected 3 task status polls (2x notPublished + 1x published)")
68+
}
69+
70+
func TestDeleteComposition_RequiresConfirmation(t *testing.T) {
71+
// Without --confirm on a non-TTY, the prompt must fail and no HTTP request must be made.
72+
r := &httpmock.Registry{}
73+
f, out := test.NewFactory(false, r, nil, "")
74+
cmd := delete.NewDeleteCmd(f)
75+
_, err := test.Execute(cmd, "my-comp", out)
76+
require.Error(t, err)
77+
assert.Empty(t, out.String())
78+
assert.Empty(t, r.Requests)
79+
}
80+
81+
func TestDeleteComposition_MissingArg(t *testing.T) {
82+
r := &httpmock.Registry{}
83+
f, out := test.NewFactory(false, r, nil, "")
84+
cmd := delete.NewDeleteCmd(f)
85+
_, err := test.Execute(cmd, "", out)
86+
require.Error(t, err)
87+
assert.Contains(t, err.Error(), "requires a <composition-id> argument")
88+
}

0 commit comments

Comments
 (0)