-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat(base): add button rule commands #2289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -119,6 +119,47 @@ func TestDryRunFieldOps(t *testing.T) { | |
| } | ||
| } | ||
|
|
||
| func TestDryRunButtonRuleOps(t *testing.T) { | ||
| ctx := context.Background() | ||
| rt := newBaseTestRuntime( | ||
| map[string]string{ | ||
| "base-token": "app_x", | ||
| "table-id": "tbl_1", | ||
| "field-id": "fld_button", | ||
| "workflow-id": "wkf_1", | ||
| }, | ||
| nil, | ||
| nil, | ||
| ) | ||
|
|
||
| assertDryRunContains(t, dryRunButtonBind(ctx, rt), "PUT /open-apis/base/v3/bases/app_x/tables/tbl_1/fields/fld_button/button_rule", `"workflow_id":"wkf_1"`) | ||
| assertDryRunContains(t, dryRunButtonGet(ctx, rt), "GET /open-apis/base/v3/bases/app_x/tables/tbl_1/fields/fld_button/button_rule") | ||
| assertDryRunContains(t, dryRunButtonUnbind(ctx, rt), "DELETE /open-apis/base/v3/bases/app_x/tables/tbl_1/fields/fld_button/button_rule") | ||
| } | ||
|
|
||
| func TestValidateButtonRuleOps(t *testing.T) { | ||
| valid := newBaseTestRuntime(map[string]string{ | ||
| "base-token": "app_x", "table-id": "tbl_1", "field-id": "fld_button", "workflow-id": "wkfAbcdefg", | ||
| }, nil, nil) | ||
| if err := validateButtonBind(valid); err != nil { | ||
| t.Fatalf("valid button bind rejected: %v", err) | ||
| } | ||
|
|
||
| badWorkflow := newBaseTestRuntime(map[string]string{ | ||
| "base-token": "app_x", "table-id": "tbl_1", "field-id": "fld_button", "workflow-id": "tblAbcdefg", | ||
| }, nil, nil) | ||
| if err := validateButtonBind(badWorkflow); err == nil || !strings.Contains(err.Error(), "wkf prefix") { | ||
| t.Fatalf("expected public workflow ID validation error, got %v", err) | ||
| } | ||
|
|
||
| missingField := newBaseTestRuntime(map[string]string{ | ||
| "base-token": "app_x", "table-id": "tbl_1", "workflow-id": "wkfAbcdefg", | ||
| }, nil, nil) | ||
| if err := validateButtonRuleLocator(missingField); err == nil || !strings.Contains(err.Error(), "--field-id") { | ||
| t.Fatalf("expected missing field validation error, got %v", err) | ||
|
Comment on lines
+151
to
+159
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Assert typed validation error metadata. These checks only inspect Assert the declared typed validation error, its category, subtype, and As per coding guidelines: “Error tests must assert typed metadata and cause preservation rather than message text alone.” 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| } | ||
| } | ||
|
|
||
| func TestDryRunRecordOps(t *testing.T) { | ||
| ctx := context.Background() | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package base | ||
|
|
||
| import ( | ||
| "context" | ||
| "strings" | ||
|
|
||
| "github.com/larksuite/cli/shortcuts/common" | ||
| ) | ||
|
|
||
| var BaseButtonBind = common.Shortcut{ | ||
| Service: "base", | ||
| Command: "+button-bind", | ||
| Description: "Bind a button field to a workflow", | ||
| Risk: "write", | ||
| Scopes: []string{"base:field:update", "base:workflow:update"}, | ||
| AuthTypes: authTypes(), | ||
| Flags: []common.Flag{ | ||
| baseTokenFlag(true), | ||
| tableRefFlag(true), | ||
| fieldRefFlag(true), | ||
| {Name: "workflow-id", Desc: "workflow ID (wkf... prefix)", Required: true}, | ||
| }, | ||
| Tips: []string{ | ||
| "Create the button-trigger workflow first, then create the button field, then bind them with this command.", | ||
| "Button field JSON must not include workflow_id; binding is managed only through button_rule APIs.", | ||
| "workflow-id must start with wkf; do not pass a tbl table ID or raw internal automation ID.", | ||
| }, | ||
| Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| return validateButtonBind(runtime) | ||
| }, | ||
| DryRun: dryRunButtonBind, | ||
| Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| return executeButtonBind(runtime) | ||
| }, | ||
| } | ||
|
|
||
| var BaseButtonGet = common.Shortcut{ | ||
| Service: "base", | ||
| Command: "+button-get", | ||
| Description: "Get the workflow bound to a button field", | ||
| Risk: "read", | ||
| Scopes: []string{"base:field:read", "base:workflow:read"}, | ||
| AuthTypes: authTypes(), | ||
| Flags: []common.Flag{ | ||
| baseTokenFlag(true), | ||
| tableRefFlag(true), | ||
| fieldRefFlag(true), | ||
| }, | ||
| Tips: []string{ | ||
| "Returns the button_rule binding for a button field; use +workflow-get for workflow details.", | ||
| "The binding workflow_id is the public wkf-prefixed workflow ID.", | ||
| }, | ||
| Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| return validateButtonRuleLocator(runtime) | ||
| }, | ||
| DryRun: dryRunButtonGet, | ||
| Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| return executeButtonGet(runtime) | ||
| }, | ||
| } | ||
|
|
||
| var BaseButtonUnbind = common.Shortcut{ | ||
| Service: "base", | ||
| Command: "+button-unbind", | ||
| Description: "Unbind a button field from its workflow", | ||
| Risk: "high-risk-write", | ||
| Scopes: []string{"base:field:update", "base:workflow:update"}, | ||
| AuthTypes: authTypes(), | ||
| Flags: []common.Flag{ | ||
| baseTokenFlag(true), | ||
| tableRefFlag(true), | ||
| fieldRefFlag(true), | ||
| }, | ||
| Tips: []string{ | ||
| "Unbind only removes the button_rule relation; it does not delete the button field or workflow.", | ||
| "Agent guidance: for high-risk writes, explain the exact target and pass --yes without asking again when the user has already asked you to perform this action.", | ||
| }, | ||
| Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| return validateButtonRuleLocator(runtime) | ||
| }, | ||
| DryRun: dryRunButtonUnbind, | ||
| Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| return executeButtonUnbind(runtime) | ||
| }, | ||
| } | ||
|
|
||
| func validateButtonRuleLocator(runtime *common.RuntimeContext) error { | ||
| if strings.TrimSpace(runtime.Str("base-token")) == "" { | ||
| return baseFlagErrorf("--base-token must not be blank") | ||
| } | ||
| if strings.TrimSpace(baseTableID(runtime)) == "" { | ||
| return baseFlagErrorf("--table-id must not be blank") | ||
| } | ||
| if strings.TrimSpace(runtime.Str("field-id")) == "" { | ||
| return baseFlagErrorf("--field-id must not be blank") | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func validateButtonBind(runtime *common.RuntimeContext) error { | ||
| if err := validateButtonRuleLocator(runtime); err != nil { | ||
| return err | ||
| } | ||
| workflowID := strings.TrimSpace(runtime.Str("workflow-id")) | ||
| if workflowID == "" { | ||
| return baseFlagErrorf("--workflow-id must not be blank") | ||
| } | ||
| if !strings.HasPrefix(workflowID, "wkf") { | ||
| return baseFlagErrorf("--workflow-id must be a public workflow ID with wkf prefix") | ||
| } | ||
|
Comment on lines
+107
to
+113
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject or normalize surrounding whitespace in
Reject leading or trailing whitespace before validation succeeds, or use the normalized value in both request bodies. Add a regression test for whitespace-padded input. Also applies to: 126-134, 152-159 🤖 Prompt for AI Agents |
||
| return nil | ||
| } | ||
|
|
||
| func buttonRulePath(runtime *common.RuntimeContext) string { | ||
| return baseV3Path( | ||
| "bases", runtime.Str("base-token"), | ||
| "tables", baseTableID(runtime), | ||
| "fields", runtime.Str("field-id"), | ||
| "button_rule", | ||
| ) | ||
| } | ||
|
|
||
| func dryRunButtonBind(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { | ||
| body := map[string]interface{}{"workflow_id": runtime.Str("workflow-id")} | ||
| return common.NewDryRunAPI(). | ||
| PUT("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields/:field_id/button_rule"). | ||
| Body(body). | ||
| Set("base_token", runtime.Str("base-token")). | ||
| Set("table_id", baseTableID(runtime)). | ||
| Set("field_id", runtime.Str("field-id")) | ||
| } | ||
|
|
||
| func dryRunButtonGet(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { | ||
| return common.NewDryRunAPI(). | ||
| GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields/:field_id/button_rule"). | ||
| Set("base_token", runtime.Str("base-token")). | ||
| Set("table_id", baseTableID(runtime)). | ||
| Set("field_id", runtime.Str("field-id")) | ||
| } | ||
|
|
||
| func dryRunButtonUnbind(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { | ||
| return common.NewDryRunAPI(). | ||
| DELETE("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields/:field_id/button_rule"). | ||
| Set("base_token", runtime.Str("base-token")). | ||
| Set("table_id", baseTableID(runtime)). | ||
| Set("field_id", runtime.Str("field-id")) | ||
| } | ||
|
|
||
| func executeButtonBind(runtime *common.RuntimeContext) error { | ||
| body := map[string]interface{}{"workflow_id": runtime.Str("workflow-id")} | ||
| data, err := baseV3CallAny(runtime, "PUT", buttonRulePath(runtime), nil, body) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| runtime.Out(map[string]interface{}{"button_rule": data, "bound": true}, nil) | ||
| return nil | ||
| } | ||
|
|
||
| func executeButtonGet(runtime *common.RuntimeContext) error { | ||
| data, err := baseV3CallAny(runtime, "GET", buttonRulePath(runtime), nil, nil) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| runtime.Out(map[string]interface{}{"button_rule": data}, nil) | ||
| return nil | ||
| } | ||
|
|
||
| func executeButtonUnbind(runtime *common.RuntimeContext) error { | ||
| data, err := baseV3CallAny(runtime, "DELETE", buttonRulePath(runtime), nil, nil) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| runtime.Out(map[string]interface{}{"button_rule": data, "unbound": true}, nil) | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -175,6 +175,8 @@ func resolveFieldTypeSpec(typeName string) (fieldTypeSpec, error) { | |
| return fieldTypeSpec{Type: "datetime", Extra: map[string]interface{}{"style": map[string]interface{}{"format": "yyyy/MM/dd"}}}, nil | ||
| case "checkbox": | ||
| return fieldTypeSpec{Type: "checkbox"}, nil | ||
| case "button", "buttonfield", "button_field", "button-field": | ||
| return fieldTypeSpec{Type: "button"}, nil | ||
|
Comment on lines
+178
to
+179
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Add regression coverage for each button alias. Add table-driven cases for As per coding guidelines: “Every behavior change requires a nearby regression test that fails when the implementation is reverted.” 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| case "user", "groupchat", "group_chat", "group-chat": | ||
| return fieldTypeSpec{Type: "user", Extra: map[string]interface{}{"multiple": true}}, nil | ||
| case "attachment": | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Add mounted dry-run and live E2E coverage for the new shortcuts.
This test calls
dryRunButtonBind,dryRunButtonGet, anddryRunButtonUnbinddirectly. It does not cover shortcut mounting, flag wiring, validation routing, confirmation behavior, orExecuterequest plumbing.Add shortcut-level dry-run E2E tests. Add self-contained live E2E coverage that creates the workflow and button field, binds, gets, unbinds, and cleans up the resources.
As per coding guidelines: “Shortcut changes require dry-run E2E coverage” and “new shortcuts require live E2E coverage.”
🤖 Prompt for AI Agents
Source: Coding guidelines