Skip to content

feat: add CompositionClient to cli and bump SDK to v4.38.0#215

Merged
ben-kalmus merged 2 commits into
algolia:mainfrom
ben-kalmus:feat/compositions
May 13, 2026
Merged

feat: add CompositionClient to cli and bump SDK to v4.38.0#215
ben-kalmus merged 2 commits into
algolia:mainfrom
ben-kalmus:feat/compositions

Conversation

@ben-kalmus
Copy link
Copy Markdown
Contributor

@ben-kalmus ben-kalmus commented May 8, 2026

Summary

  • Adds a new compositions command group to the Algolia CLI covering the full management lifecycle for Algolia Compositions (https://www.algolia.com/doc/rest-api/composition/).

  • This also bumps the Algolia Go SDK from v4.35.0 to v4.38.0 to pick up the composition package.

Commands added

Composition management:

algolia compositions list
algolia compositions get <composition-id>
algolia compositions upsert <composition-id> --file body.json
algolia compositions delete <composition-id>
algolia compositions search <composition-id> <query>

Composition rule management:

algolia compositions rules list <composition-id>
algolia compositions rules get <composition-id> <rule-id>
algolia compositions rules upsert <composition-id> <rule-id> --file rule.json
algolia compositions rules delete <composition-id> <rule-id>

Design notes

  • All write operations (upsert, delete) always wait for the task to reach published status before returning. There is no --wait flag unlike indices command group.
  • Delete commands prompt for confirmation unless --confirm/-y is passed.
  • ACLs are enforced: read commands require search, write commands require editSettings.

Changes

  • go.mod / go.sum: Bump algoliasearch-client-go/v4 to v4.38.0
  • pkg/cmdutil/factory.go: Add CompositionClient field
  • pkg/cmd/factory/default.go: Implement compositionClient() factory function
  • pkg/cmd/root/root.go: Register NewCompositionsCmd
  • pkg/cmd/compositions/: New command group

Tests

Each command has a unit test file covering:

  • Happy path with exact JSON output assertion
  • Missing required argument(s) - verifies descriptive error message
  • For upsert commands: missing --file flag, invalid JSON input
  • For delete commands: confirmation required

End to end test script:
e2e/testscripts/compositions/compositions.txtar

@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented May 8, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 95 complexity · 153 duplication

Metric Results
Complexity 95
Duplication 153

View in Codacy

TIP This summary will be updated as you push new changes.

@ben-kalmus ben-kalmus force-pushed the feat/compositions branch from 5e25461 to 9c9f982 Compare May 8, 2026 13:33
@ben-kalmus ben-kalmus marked this pull request as ready for review May 8, 2026 13:34
@ben-kalmus ben-kalmus force-pushed the feat/compositions branch 2 times, most recently from 34d611e to 44670b1 Compare May 8, 2026 16:02
@ben-kalmus ben-kalmus force-pushed the feat/compositions branch from 44670b1 to 1b105f3 Compare May 13, 2026 12:45
@ben-kalmus ben-kalmus requested review from LorrisSaintGenez and dylantientcheu and removed request for dylantientcheu May 13, 2026 12:59
Copy link
Copy Markdown
Collaborator

@NatanTechofNY NatanTechofNY left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-Generated Code Review by Claude Code

Overview

Large new feature: 29 files, ~1870 additions adding a full compositions command group (list, get, upsert, delete, search + rules sub-group) with SDK bump to v4.38.0. Well-structured and follows repo conventions closely, with strong unit test coverage. A few consistency gaps versus existing delete commands and one blocking issue (E2E tests won't actually run).

✅ Strengths

  • Follows the exact repo pattern: New...Cmd, RunE, cmdutil.PrintFlags, validators.ExactArgsWithMsg, heredoc examples
  • Thorough unit tests for every command including timeout, API error, and confirmation cases
  • Wait-always design is cleaner UX for compositions than opt-in --wait
  • ACL annotations correct: read = search, write = editSettings

📋 Summary of Findings

  • 🚨 HIGH: E2E test script never runs — TestCompositions missing in e2e/e2e_test.go
  • ⚠️ MEDIUM (×4): CanPrompt() guard missing; ErrCancel on decline; prompt error wrapping inconsistency; upsert prints taskID after wait
  • 💡 LOW: First poll waits full interval before first GetTask call

Generated by Claude Code — automated review focusing on security, performance, and best practices

@@ -0,0 +1,94 @@
env COMP_ID=test-comp-e2e
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 HIGH: E2E test script is never executed

This txtar file exists, but e2e/e2e_test.go has no TestCompositions function. The runner only picks up directories explicitly registered via runTestsInDir. The file currently registers 8 tests (TestVersion, TestIndices, TestSettings, TestObjects, TestSynonyms, TestRules, TestSearch, TestAgentReady) — compositions is absent.

// Add to e2e/e2e_test.go alongside the other Test* functions:
func TestCompositions(t *testing.T) {
    runTestsInDir(t, "testscripts/compositions")
}

Without this, go test ./e2e -tags=e2e silently skips all compositions txtar scripts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1b105f3. Added TestCompositions to e2e/e2e_test.go so the txtar script runs.

opts.CompositionID = args[0]

if !opts.DoConfirm {
var confirmed bool
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ MEDIUM: Missing CanPrompt() guard before interactive prompt

Every other delete command in the repo (indices/delete, rules/delete, synonyms/delete, etc.) checks opts.IO.CanPrompt() before attempting an interactive prompt, and returns a clear error in non-TTY contexts:

// from pkg/cmd/rules/delete/delete.go (existing pattern)
if !confirm {
    if !opts.IO.CanPrompt() {
        return cmdutil.FlagErrorf("--confirm required when non-interactive shell is detected")
    }
    opts.DoConfirm = true
}

Without this check, running in CI or a script without --confirm will call prompt.Confirm on a non-TTY and fail with a cryptic survey error. Please apply the same guard here (and in compositions/rules/delete).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added CanPrompt() guard to both compositions/delete and compositions/rules/delete, returning cmdutil.FlagErrorf in non-interactive contexts to match the repo pattern.

}
if !confirmed {
return cmdutil.ErrCancel
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ MEDIUM: ErrCancel on decline causes exit code 2 — inconsistent with repo convention

Returning cmdutil.ErrCancel here triggers exit code 2 via root.go's IsUserCancellation mapping. Every other delete command in the repo (indices, rules, synonyms, objects, dictionary, apikeys) returns nil (exit 0) when a user declines — the operation just doesn't happen.

// Change to:
if !confirmed {
    return nil
}

Scripts checking $? after a declined algolia compositions delete will see an unexpected failure otherwise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed both delete commands to return nil on decline instead of cmdutil.ErrCancel, matching the convention in indices/delete, rules/delete, etc.

)
if err != nil {
return err
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ MEDIUM: Prompt error returned unwrapped — inconsistent with sibling command

In compositions/delete/delete.go, the equivalent block wraps the error:
return fmt.Errorf("failed to prompt: %w", err)

Here it's returned bare: return err. These are adjacent files in the same PR — pick one style and apply it consistently.

// Change to match compositions/delete:
return fmt.Errorf("failed to prompt: %w", err)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapped the prompt error with fmt.Errorf("failed to prompt: %w", err) to match the sibling compositions/delete style.

}

return p.Print(opts.IO, res)
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ MEDIUM: Upsert prints task response after waiting — atypical pattern

After WaitForTask succeeds, p.Print(opts.IO, res) prints the original mutation API response (e.g. {"taskID": 42}). Across the rest of the repo, mutate+wait commands either print a structured summary map (objects/update, rules/import) or a TTY success message (synonyms/save, settings/set) — raw task responses are not the post-wait output pattern.

Users may be confused seeing a taskID when they expected confirmation of the composition state. Consider printing a brief success message (TTY) or a summary struct (non-TTY), or at minimum document in Short/Long that the output is the async task result. Same applies to rule upsert and both delete commands.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a TTY-only success message after WaitForTask for all four mutate commands (upsert + delete, composition + rule). The structured JSON (taskID) is still emitted via PrintFlags for scripting, but TTY users now get explicit confirmation that the operation completed.

return err
}
if resp.Status == algoliaComposition.TASK_STATUS_PUBLISHED {
io.StopProgressIndicator()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 LOW: First GetTask poll waits a full PollInterval (2s) before firing

time.NewTicker does not fire immediately — the first tick arrives after one pollInterval. For tasks that complete quickly, this adds up to 2s of unnecessary latency.

Consider issuing one immediate GetTask call before entering the ticker loop:

// Immediate first check before polling loop
resp, err := client.GetTask(client.NewApiGetTaskRequest(compositionID, taskID))
if err != nil { ... }
if resp.Status == algoliaComposition.TASK_STATUS_PUBLISHED {
    io.StopProgressIndicator()
    return nil
}
// then start the ticker loop

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an immediate GetTask call before entering the ticker loop in WaitForTask. Quick-completing tasks now return without waiting a full pollInterval. Existing tests still pass (mock response order is preserved).

- 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>
Copy link
Copy Markdown
Collaborator

@NatanTechofNY NatanTechofNY left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI Follow-up Review by Claude Code (commit 2ba948b)

All 6 findings from the previous round have been addressed — thank you for the fast turnaround.


✅ Previous issues resolved

Issue Status
🚨 E2E test never ran — missing TestCompositions in e2e/e2e_test.go ✅ Fixed
⚠️ Missing CanPrompt() guard before interactive prompt in both delete commands ✅ Fixed
⚠️ ErrCancel on decline → exit code 2 (inconsistent with rest of repo) ✅ Fixed — now returns nil
⚠️ Prompt error unwrapped in rules/delete ✅ Fixed — now wrapped with fmt.Errorf("failed to prompt: %w")
⚠️ Upsert/delete printed raw task response with no UX signal ✅ Fixed — TTY success message added
💡 First GetTask poll wasted a full PollInterval ✅ Fixed — immediate first check added before ticker loop

💡 Minor nit (non-blocking)

On TTY, mutate commands (upsert, rules upsert, delete, rules delete) currently print both a success line and the JSON task response:

✓ Upserted composition my-comp
{"taskID":42}

The success message is a nice touch, but the JSON that follows (from p.Print(opts.IO, res)) makes the TTY output a little noisy. Other async commands in the repo (e.g. synonyms/save) print only the success message on TTY and return nil. This is a style preference — either keeping both (useful for piping) or suppressing the JSON on TTY would be reasonable. Up to you.


Approved. Clean implementation, good test coverage, and all review feedback addressed. 🎉


Generated by Claude Code — automated review

@ben-kalmus ben-kalmus merged commit 29a9b64 into algolia:main May 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants