Skip to content

Add bulk key operations: bulk-create, bulk-update, bulk-delete#212

Open
lechuckcaptain wants to merge 4 commits intolokalise:mainfrom
lechuckcaptain:feature/bulk-key-operations
Open

Add bulk key operations: bulk-create, bulk-update, bulk-delete#212
lechuckcaptain wants to merge 4 commits intolokalise:mainfrom
lechuckcaptain:feature/bulk-key-operations

Conversation

@lechuckcaptain
Copy link
Copy Markdown

@lechuckcaptain lechuckcaptain commented Mar 17, 2026

Problem

The Lokalise API supports bulk operations on keys — creating multiple keys in a single request, updating multiple keys at once, and deleting multiple keys by ID. The underlying Go SDK (go-lokalise-api/v4) already exposes these capabilities:

  • Keys().Create(projectID, []NewKey{...}) — accepts a slice of keys
  • Keys().BulkUpdate(projectID, []BulkUpdateKey{...}) — dedicated bulk update method
  • Keys().BulkDelete(projectID, []int64{...}) — dedicated bulk delete method

However, the CLI only wraps these as single-item operations:

  • key create wraps one NewKey in a slice
  • key update calls Keys().Update() (single key only)
  • key delete calls Keys().Delete() (single key only)

This forces users who need to operate on many keys to loop over individual CLI calls, resulting in N API requests instead of 1. For projects with hundreds or thousands of keys, this is a significant usability and performance gap — especially for common workflows like batch tagging, archiving unused keys, or bootstrapping a new project with a predefined key set.

Solution

Three new subcommands under key:

key bulk-delete

Accepts a comma-separated list of key IDs and calls Keys().BulkDelete().

lokalise2 key bulk-delete --project-id <id> --key-ids 123,456,789

key bulk-update

Accepts a JSON array of BulkUpdateKey objects (each containing key_id and fields to update) and calls Keys().BulkUpdate().

lokalise2 key bulk-update --project-id <id> \
  --keys '[{"key_id":123,"description":"Updated","tags":["v2"]},{"key_id":456,"is_hidden":true}]'

key bulk-create

Accepts a JSON array of NewKey objects and calls Keys().Create() with the full slice.

lokalise2 key bulk-create --project-id <id> \
  --keys '[{"key_name":"welcome","platforms":["web"],"tags":[]},{"key_name":"goodbye","platforms":["ios"],"tags":["v2"]}]'

Both bulk-update and bulk-create support the existing --use-automations flag (default true).

Design decisions

  • New subcommands instead of modifying existing onesbulk-delete and bulk-update map to distinct SDK methods with different signatures. Keeping them separate avoids backward-compatibility concerns and keeps each command focused.
  • --key-ids as IntSlice for bulk-delete — follows the existing pattern from team-user-group add-members (IntSliceVar + int-to-int64 conversion).
  • --keys as JSON string for bulk-update/create — follows the existing pattern of JSON string flags (--translations, --filenames). For large payloads, users can use --keys "$(cat keys.json)".
  • Existing commands untouchedkey create, key update, and key delete are not modified. Zero breaking changes.

Test plan

  • Unit tests added (cmd/key_test.go) covering all three commands + invalid JSON error handling
  • go test ./... passes
  • go build ./... passes
  • Verified end-to-end against a live Lokalise project:
    • bulk-create created 2 keys in one API call
    • bulk-update updated descriptions and tags on both keys in one call
    • bulk-delete deleted both keys in one call (keys_removed: true)

Expose the SDK's BulkUpdate, BulkDelete, and multi-key Create methods
as new CLI subcommands, closing the gap between API and CLI
capabilities.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 17, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: lokalise/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 62fa98ed-0671-40b0-b819-4aa722a3c869

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds three new CLI subcommands under key: bulk-delete, bulk-update, and bulk-create. Each command wires flags, performs PreRun client initialization, parses inputs (JSON arrays for create/update, int64 slice for delete), validates non-empty payloads where required, and calls the API client: Api.Keys().BulkDelete, Api.Keys().BulkUpdate (optionally with automations), or Api.Keys().Create (optionally with automations). Unit tests were added for successful flows, automations toggles, and invalid/empty JSON payload error cases.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant CLI
participant ApiClient
participant RemoteAPI
User->>CLI: invoke key bulk-create|bulk-update|bulk-delete with flags
CLI->>CLI: PreRun (init Api client, parse flags, unmarshal JSON if needed, validate)
CLI->>ApiClient: call Keys().BulkCreate/BulkUpdate/BulkDelete(projectId, payload, opts)
ApiClient->>RemoteAPI: HTTP request to /api2/projects/{projectId}/keys
RemoteAPI-->>ApiClient: HTTP response (JSON or error)
ApiClient-->>CLI: parsed response or error
CLI-->>User: print JSON response or error

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding three new bulk key operation subcommands (bulk-create, bulk-update, bulk-delete) to the CLI.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing the problem, solution, design decisions, and test coverage for the bulk key operations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
cmd/key_test.go (1)

51-79: Strengthen update/create tests with outbound payload assertions.

Lines 51-79 and 97-141 only verify method/header. Adding testBody(...) checks (like Line 27 in bulk-delete) would catch regressions in request marshaling for the two new commands.

Also applies to: 97-141

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/key_test.go` around lines 51 - 79, The PUT and the corresponding create
handler in cmd/key_test.go currently only assert method and header; add testBody
assertions (using the existing testBody(t, r, expectedJSON) helper like in the
bulk-delete test) to both handlers to validate the outbound request payloads for
the update and create commands; locate the mux.HandleFunc blocks that use
fmt.Sprintf("/api2/projects/%s/keys", testProjectID) and the second keys handler
near the other test and add testBody calls with the expected JSON payload
(matching what the CLI should send) immediately after testHeader so the request
marshaling is asserted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/key.go`:
- Around line 22-26: The shared package-level boolean useAutomations is being
bound to multiple Cobra commands (keyCreateCmd, keyBulkUpdateCmd,
keyBulkCreateCmd) causing flag state to leak across commands; introduce distinct
backing variables (e.g., useAutomationsCreate, useAutomationsBulkUpdate,
useAutomationsBulkCreate), bind each command's --use-automations flag to its own
variable, and update the places that read the flag (the handlers referencing
useAutomations) to use the corresponding new variable for keyCreateCmd,
keyBulkUpdateCmd, and keyBulkCreateCmd so each command's flag is decoupled.
- Line 24: Change the bulkDeleteKeyIds declaration from []int to []int64 and
remove the manual conversion that builds a []int64 before the API call; update
the CLI flag registration to use Int64SliceVar instead of IntSliceVar so the
flag directly populates bulkDeleteKeyIds. Specifically, modify the variable
named bulkDeleteKeyIds, delete the conversion loop that casts []int to []int64,
ensure places that consume bulkDeleteKeyIds (the bulk delete handler) accept
[]int64, and replace any flag usage of IntSliceVar with Int64SliceVar so
single-key operations (keyId) and bulk operations use int64 end-to-end.

---

Nitpick comments:
In `@cmd/key_test.go`:
- Around line 51-79: The PUT and the corresponding create handler in
cmd/key_test.go currently only assert method and header; add testBody assertions
(using the existing testBody(t, r, expectedJSON) helper like in the bulk-delete
test) to both handlers to validate the outbound request payloads for the update
and create commands; locate the mux.HandleFunc blocks that use
fmt.Sprintf("/api2/projects/%s/keys", testProjectID) and the second keys handler
near the other test and add testBody calls with the expected JSON payload
(matching what the CLI should send) immediately after testHeader so the request
marshaling is asserted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: lokalise/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4659508e-21cc-4378-a17b-89487b073f3e

📥 Commits

Reviewing files that changed from the base of the PR and between d93a5dc and 84d7289.

📒 Files selected for processing (2)
  • cmd/key.go
  • cmd/key_test.go

Comment thread cmd/key.go Outdated
Comment thread cmd/key.go Outdated
…ions

- Decouple use-automations flag by introducing per-command backing variables
  (bulkUpdateUseAutomations, bulkCreateUseAutomations) to prevent flag state
  leaking across commands
- Change bulkDeleteKeyIds from []int to []int64 with Int64SliceVar, removing
  the manual conversion loop for consistency with single-key operations
- Add testBody assertions to TestKeyBulkUpdate and TestKeyBulkCreate to
  validate outbound request payloads
@lechuckcaptain
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 18, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
cmd/key.go (1)

3-8: Harden --keys validation and error messages before API calls

Both bulk handlers currently return raw unmarshal errors and accept [] silently. Add CLI-side validation so users get actionable errors and empty payloads fail fast.

💡 Proposed refactor
 import (
 	"encoding/json"
+	"fmt"

 	"github.com/lokalise/go-lokalise-api/v4"
 	"github.com/spf13/cobra"
 )
@@
 	RunE: func(*cobra.Command, []string) error {
 		var keys []lokalise.BulkUpdateKey
 		if err := json.Unmarshal([]byte(bulkUpdateKeys), &keys); err != nil {
-			return err
+			return fmt.Errorf("invalid value for --keys: expected JSON array of bulk update keys: %w", err)
+		}
+		if len(keys) == 0 {
+			return fmt.Errorf("--keys must contain at least one key object")
 		}
@@
 	RunE: func(*cobra.Command, []string) error {
 		var keys []lokalise.NewKey
 		if err := json.Unmarshal([]byte(bulkCreateKeys), &keys); err != nil {
-			return err
+			return fmt.Errorf("invalid value for --keys: expected JSON array of new keys: %w", err)
+		}
+		if len(keys) == 0 {
+			return fmt.Errorf("--keys must contain at least one key object")
 		}

Also applies to: 160-163, 182-185

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/key.go` around lines 3 - 8, The CLI should validate the --keys flag
payload before making API calls: locate the code that reads the --keys flag and
calls json.Unmarshal (the bulk handlers that unmarshal into a keys slice) and
add pre-checks that the raw string is non-empty and not just "[]" (trim
whitespace and reject if empty or equals "[]"); then perform json.Unmarshal but
wrap any error with a user-friendly message like "failed to parse --keys JSON:
<err>" and after unmarshalling verify the resulting slice length > 0 and return
a clear error ("--keys payload must contain at least one key") instead of
letting empty slices or raw unmarshal errors propagate to the API. Ensure these
checks are applied to both bulk handlers that currently perform json.Unmarshal
so the API is never called with an empty payload.
cmd/key_test.go (1)

47-96: Add explicit --use-automations=false coverage

Right now only the default true path is asserted. Add one test each for bulk-update and bulk-create with --use-automations=false, and assert "use_automations":false in the request body to lock in the flag behavior.

Also applies to: 98-163

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/key_test.go` around lines 47 - 96, Add new test cases alongside
TestKeyBulkUpdate to cover the explicit false path for both bulk-update and
bulk-create: duplicate the existing TestKeyBulkUpdate pattern (and the
bulk-create test around lines 98-163) to create
TestKeyBulkUpdate_UseAutomationsFalse and TestKeyBulkCreate_UseAutomationsFalse;
set args to include "--use-automations=false" (e.g. args :=
[]string{"key","bulk-update","--keys=...","--project-id="+testProjectID,"--use-automations=false"})
and in the mux handler for each test assert the request body contains
`"use_automations":false` (use the same json compact + testBody approach used in
TestKeyBulkUpdate), and ensure keyBulkUpdateCmd.PreRun / keyBulkCreateCmd.PreRun
set Api = client before executing rootCmd.Execute() so the client is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmd/key_test.go`:
- Around line 47-96: Add new test cases alongside TestKeyBulkUpdate to cover the
explicit false path for both bulk-update and bulk-create: duplicate the existing
TestKeyBulkUpdate pattern (and the bulk-create test around lines 98-163) to
create TestKeyBulkUpdate_UseAutomationsFalse and
TestKeyBulkCreate_UseAutomationsFalse; set args to include
"--use-automations=false" (e.g. args :=
[]string{"key","bulk-update","--keys=...","--project-id="+testProjectID,"--use-automations=false"})
and in the mux handler for each test assert the request body contains
`"use_automations":false` (use the same json compact + testBody approach used in
TestKeyBulkUpdate), and ensure keyBulkUpdateCmd.PreRun / keyBulkCreateCmd.PreRun
set Api = client before executing rootCmd.Execute() so the client is used.

In `@cmd/key.go`:
- Around line 3-8: The CLI should validate the --keys flag payload before making
API calls: locate the code that reads the --keys flag and calls json.Unmarshal
(the bulk handlers that unmarshal into a keys slice) and add pre-checks that the
raw string is non-empty and not just "[]" (trim whitespace and reject if empty
or equals "[]"); then perform json.Unmarshal but wrap any error with a
user-friendly message like "failed to parse --keys JSON: <err>" and after
unmarshalling verify the resulting slice length > 0 and return a clear error
("--keys payload must contain at least one key") instead of letting empty slices
or raw unmarshal errors propagate to the API. Ensure these checks are applied to
both bulk handlers that currently perform json.Unmarshal so the API is never
called with an empty payload.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: lokalise/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c06b2c44-b96d-4157-a628-ca1b08008d17

📥 Commits

Reviewing files that changed from the base of the PR and between 84d7289 and 87c549f.

📒 Files selected for processing (2)
  • cmd/key.go
  • cmd/key_test.go

Reject empty JSON arrays for --keys on bulk-update and bulk-create before
hitting the API. Add unit tests for --use-automations=false path and empty
keys validation.
@lechuckcaptain
Copy link
Copy Markdown
Author

@coderabbitai review 🙏

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 19, 2026

@lechuckcaptain: Sure! Triggering a review of the PR now. 🐇

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
cmd/key.go (1)

143-154: Consider validating non-empty key IDs for consistency.

Unlike bulk-update and bulk-create which validate len(keys) == 0, bulk-delete doesn't check for an empty bulkDeleteKeyIds slice. While the flag is marked required, passing --key-ids= could result in an empty slice being sent to the API.

♻️ Suggested validation for consistency
 var keyBulkDeleteCmd = &cobra.Command{
 	Use:   "bulk-delete",
 	Short: "Delete multiple keys",
 	Long:  "Deletes multiple keys from the project. Requires Manage keys admin right.",
 	RunE: func(*cobra.Command, []string) error {
+		if len(bulkDeleteKeyIds) == 0 {
+			return errors.New("--key-ids must contain at least one key ID")
+		}
 		resp, err := Api.Keys().BulkDelete(projectId, bulkDeleteKeyIds)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/key.go` around lines 143 - 154, The keyBulkDeleteCmd handler should
validate that bulkDeleteKeyIds is non-empty before calling
Api.Keys().BulkDelete: add the same check used in bulk-update/bulk-create to
return an error when len(bulkDeleteKeyIds) == 0 (or when the parsed flag results
in an empty slice) and avoid calling Api.Keys().BulkDelete/printJson with an
empty list; reference the keyBulkDeleteCmd, the bulkDeleteKeyIds variable, and
the Api.Keys().BulkDelete invocation when making the change.
cmd/key_test.go (2)

36-44: Consider resetting command state between tests for better isolation.

Tests mutate shared Cobra command state (rootCmd.SetArgs(), *.PreRun). While each test explicitly sets all required args, flag values from previous tests could persist if a test fails mid-execution. This is a broader pattern in the codebase, not specific to this PR.

A common mitigation is to reset flags or use subtests with fresh command instances. However, since this follows existing patterns in the repository, it's acceptable to defer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/key_test.go` around lines 36 - 44, The test mutates shared Cobra state
(rootCmd.SetArgs and keyBulkDeleteCmd.PreRun) which can leak between tests; fix
by saving and restoring the mutated state around the test: capture the original
rootCmd args (call rootCmd.Args or the current args slice) and original
keyBulkDeleteCmd.PreRun, then use defer to restore them after the test, or
explicitly clear args with rootCmd.SetArgs(nil) and reset
keyBulkDeleteCmd.PreRun = nil at the end of the test to ensure isolation for
functions rootCmd.SetArgs and keyBulkDeleteCmd.PreRun.

233-295: Consider asserting specific error messages for robustness.

The error path tests verify that an error occurs but don't validate the error content. This could mask regressions where the command fails for unexpected reasons.

♻️ Example: validate error message for empty keys test
 	err := rootCmd.Execute()
 	if err == nil {
 		t.Error("Expected error for empty keys, got nil")
 	}
+	if err != nil && err.Error() != "--keys must contain at least one key object" {
+		t.Errorf("Expected empty keys error, got: %v", err)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/key_test.go` around lines 233 - 295, The tests
TestKeyBulkUpdate_InvalidJSON, TestKeyBulkCreate_InvalidJSON,
TestKeyBulkUpdate_EmptyKeys and TestKeyBulkCreate_EmptyKeys only check that an
error occurred but not that it is the expected error; update each test that
calls rootCmd.Execute (with keyBulkUpdateCmd.PreRun or keyBulkCreateCmd.PreRun)
to assert the returned error message or type matches the expected failure (e.g.,
"invalid JSON" for malformed --keys and "no keys provided" or similar for empty
arrays) using t.Fatalf/t.Errorf or your test helper (require/expect) so the
tests fail if the command errors for an unexpected reason; reference the
specific commands keyBulkUpdateCmd and keyBulkCreateCmd and validate err.Error()
(or wrapped error check) against the canonical message/identifier your CLI
returns.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmd/key_test.go`:
- Around line 36-44: The test mutates shared Cobra state (rootCmd.SetArgs and
keyBulkDeleteCmd.PreRun) which can leak between tests; fix by saving and
restoring the mutated state around the test: capture the original rootCmd args
(call rootCmd.Args or the current args slice) and original
keyBulkDeleteCmd.PreRun, then use defer to restore them after the test, or
explicitly clear args with rootCmd.SetArgs(nil) and reset
keyBulkDeleteCmd.PreRun = nil at the end of the test to ensure isolation for
functions rootCmd.SetArgs and keyBulkDeleteCmd.PreRun.
- Around line 233-295: The tests TestKeyBulkUpdate_InvalidJSON,
TestKeyBulkCreate_InvalidJSON, TestKeyBulkUpdate_EmptyKeys and
TestKeyBulkCreate_EmptyKeys only check that an error occurred but not that it is
the expected error; update each test that calls rootCmd.Execute (with
keyBulkUpdateCmd.PreRun or keyBulkCreateCmd.PreRun) to assert the returned error
message or type matches the expected failure (e.g., "invalid JSON" for malformed
--keys and "no keys provided" or similar for empty arrays) using
t.Fatalf/t.Errorf or your test helper (require/expect) so the tests fail if the
command errors for an unexpected reason; reference the specific commands
keyBulkUpdateCmd and keyBulkCreateCmd and validate err.Error() (or wrapped error
check) against the canonical message/identifier your CLI returns.

In `@cmd/key.go`:
- Around line 143-154: The keyBulkDeleteCmd handler should validate that
bulkDeleteKeyIds is non-empty before calling Api.Keys().BulkDelete: add the same
check used in bulk-update/bulk-create to return an error when
len(bulkDeleteKeyIds) == 0 (or when the parsed flag results in an empty slice)
and avoid calling Api.Keys().BulkDelete/printJson with an empty list; reference
the keyBulkDeleteCmd, the bulkDeleteKeyIds variable, and the
Api.Keys().BulkDelete invocation when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: lokalise/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 92c0f63a-ef91-4c4d-b0ef-94f97dc2ed23

📥 Commits

Reviewing files that changed from the base of the PR and between 87c549f and 3c67830.

📒 Files selected for processing (2)
  • cmd/key.go
  • cmd/key_test.go

@lechuckcaptain lechuckcaptain force-pushed the feature/bulk-key-operations branch from e66a3cc to 2b9a549 Compare March 20, 2026 09:21
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.

1 participant