mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
b07be60068
Emit structured validation, API, network, file, and internal error envelopes for Sheets shortcuts so users and agents can recover from failed spreadsheet workflows using stable type, subtype, param, and code fields. Add Sheets domain errscontract and golangci guards to prevent legacy envelope and common helper regressions.
335 lines
12 KiB
Go
335 lines
12 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package backward
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/validate"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
func dataValidationBasePath(token string) string {
|
|
return fmt.Sprintf("/open-apis/sheets/v2/spreadsheets/%s/dataValidation",
|
|
validate.EncodePathSegment(token))
|
|
}
|
|
|
|
func dataValidationSheetPath(token, sheetID string) string {
|
|
return fmt.Sprintf("%s/%s", dataValidationBasePath(token), validate.EncodePathSegment(sheetID))
|
|
}
|
|
|
|
func validateDropdownToken(runtime *common.RuntimeContext) (string, error) {
|
|
token := runtime.Str("spreadsheet-token")
|
|
if runtime.Str("url") != "" {
|
|
token = extractSpreadsheetToken(runtime.Str("url"))
|
|
}
|
|
if token == "" {
|
|
return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "specify --url or --spreadsheet-token").WithParams(errs.InvalidParam{Name: "--url", Reason: "required; specify one"}, errs.InvalidParam{Name: "--spreadsheet-token", Reason: "required; specify one"})
|
|
}
|
|
return token, nil
|
|
}
|
|
|
|
func parseJSONStringArray(flagName, value string) ([]interface{}, error) {
|
|
var typed []string
|
|
if err := json.Unmarshal([]byte(value), &typed); err != nil {
|
|
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--%s must be a JSON array of strings: %v", flagName, err).WithParam("--" + flagName)
|
|
}
|
|
if typed == nil {
|
|
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--%s must be a JSON array, got null", flagName).WithParam("--" + flagName)
|
|
}
|
|
arr := make([]interface{}, len(typed))
|
|
for i, s := range typed {
|
|
arr[i] = s
|
|
}
|
|
return arr, nil
|
|
}
|
|
|
|
func validateRangesFlag(runtime *common.RuntimeContext) ([]interface{}, error) {
|
|
ranges, err := parseJSONStringArray("ranges", runtime.Str("ranges"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(ranges) == 0 {
|
|
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--ranges must not be empty").WithParam("--ranges")
|
|
}
|
|
for i, r := range ranges {
|
|
s, _ := r.(string)
|
|
if _, _, ok := splitSheetRange(s); !ok {
|
|
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--ranges[%d] %q must be a fully qualified range with sheet ID prefix (e.g. <sheetId>!A2:A100)", i, s).WithParam("--ranges")
|
|
}
|
|
}
|
|
return ranges, nil
|
|
}
|
|
|
|
func buildDropdownBody(runtime *common.RuntimeContext) (map[string]interface{}, error) {
|
|
condValues, err := parseJSONStringArray("condition-values", runtime.Str("condition-values"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(condValues) == 0 {
|
|
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--condition-values must not be empty").WithParam("--condition-values")
|
|
}
|
|
|
|
dv := map[string]interface{}{
|
|
"conditionValues": condValues,
|
|
}
|
|
|
|
opts := map[string]interface{}{}
|
|
if runtime.Cmd.Flags().Changed("multiple") {
|
|
opts["multipleValues"] = runtime.Bool("multiple")
|
|
}
|
|
if runtime.Cmd.Flags().Changed("highlight") {
|
|
opts["highlightValidData"] = runtime.Bool("highlight")
|
|
}
|
|
if runtime.Str("colors") != "" {
|
|
colors, err := parseJSONStringArray("colors", runtime.Str("colors"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(colors) != len(condValues) {
|
|
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--colors length (%d) must match --condition-values length (%d)", len(colors), len(condValues)).WithParam("--colors")
|
|
}
|
|
opts["colors"] = colors
|
|
}
|
|
if len(opts) > 0 {
|
|
dv["options"] = opts
|
|
}
|
|
|
|
return dv, nil
|
|
}
|
|
|
|
// SheetSetDropdown sets dropdown list validation on a range.
|
|
var SheetSetDropdown = common.Shortcut{
|
|
Service: "sheets",
|
|
Command: "+set-dropdown",
|
|
Description: "Set dropdown list on a cell range",
|
|
Risk: "write",
|
|
Scopes: []string{"sheets:spreadsheet:write_only", "sheets:spreadsheet:read"},
|
|
AuthTypes: []string{"user", "bot"},
|
|
Flags: []common.Flag{
|
|
{Name: "url", Desc: "spreadsheet URL"},
|
|
{Name: "spreadsheet-token", Desc: "spreadsheet token"},
|
|
{Name: "range", Desc: "cell range (<sheetId>!A2:A100)", Required: true},
|
|
{Name: "condition-values", Desc: `dropdown options as JSON array (e.g. '["opt1","opt2"]'), max 500, each <=100 chars, no commas`, Required: true},
|
|
{Name: "multiple", Desc: "enable multi-select (default false)", Type: "bool"},
|
|
{Name: "highlight", Desc: "color-code options (default false)", Type: "bool"},
|
|
{Name: "colors", Desc: `RGB hex color array (e.g. '["#1FB6C1","#F006C2"]'), must match condition-values length`},
|
|
},
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
if _, err := validateDropdownToken(runtime); err != nil {
|
|
return err
|
|
}
|
|
if _, _, ok := splitSheetRange(runtime.Str("range")); !ok {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--range must be a fully qualified range with sheet ID prefix (e.g. <sheetId>!A2:A100)").WithParam("--range")
|
|
}
|
|
_, err := buildDropdownBody(runtime)
|
|
return err
|
|
},
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
token, _ := validateDropdownToken(runtime)
|
|
dv, _ := buildDropdownBody(runtime)
|
|
return common.NewDryRunAPI().
|
|
POST("/open-apis/sheets/v2/spreadsheets/:token/dataValidation").
|
|
Body(map[string]interface{}{
|
|
"range": runtime.Str("range"),
|
|
"dataValidationType": "list",
|
|
"dataValidation": dv,
|
|
}).
|
|
Set("token", token)
|
|
},
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
token, _ := validateDropdownToken(runtime)
|
|
dv, err := buildDropdownBody(runtime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
data, err := runtime.CallAPITyped("POST", dataValidationBasePath(token), nil,
|
|
map[string]interface{}{
|
|
"range": runtime.Str("range"),
|
|
"dataValidationType": "list",
|
|
"dataValidation": dv,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
runtime.Out(data, nil)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// SheetUpdateDropdown updates dropdown list settings for given ranges.
|
|
var SheetUpdateDropdown = common.Shortcut{
|
|
Service: "sheets",
|
|
Command: "+update-dropdown",
|
|
Description: "Update dropdown list settings",
|
|
Risk: "write",
|
|
Scopes: []string{"sheets:spreadsheet:write_only", "sheets:spreadsheet:read"},
|
|
AuthTypes: []string{"user", "bot"},
|
|
Flags: []common.Flag{
|
|
{Name: "url", Desc: "spreadsheet URL"},
|
|
{Name: "spreadsheet-token", Desc: "spreadsheet token"},
|
|
{Name: "sheet-id", Desc: "sheet ID", Required: true},
|
|
{Name: "ranges", Desc: `ranges as JSON array (e.g. '["sheetId!A1:A100"]')`, Required: true},
|
|
{Name: "condition-values", Desc: `dropdown options as JSON array (e.g. '["opt1","opt2"]')`, Required: true},
|
|
{Name: "multiple", Desc: "enable multi-select (default false)", Type: "bool"},
|
|
{Name: "highlight", Desc: "color-code options (default false)", Type: "bool"},
|
|
{Name: "colors", Desc: `RGB hex color array, must match condition-values length`},
|
|
},
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
if _, err := validateDropdownToken(runtime); err != nil {
|
|
return err
|
|
}
|
|
if _, err := validateRangesFlag(runtime); err != nil {
|
|
return err
|
|
}
|
|
_, err := buildDropdownBody(runtime)
|
|
return err
|
|
},
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
token, _ := validateDropdownToken(runtime)
|
|
ranges, _ := parseJSONStringArray("ranges", runtime.Str("ranges"))
|
|
dv, _ := buildDropdownBody(runtime)
|
|
return common.NewDryRunAPI().
|
|
PUT("/open-apis/sheets/v2/spreadsheets/:token/dataValidation/:sheet_id").
|
|
Body(map[string]interface{}{
|
|
"ranges": ranges,
|
|
"dataValidationType": "list",
|
|
"dataValidation": dv,
|
|
}).
|
|
Set("token", token).Set("sheet_id", runtime.Str("sheet-id"))
|
|
},
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
token, _ := validateDropdownToken(runtime)
|
|
ranges, err := parseJSONStringArray("ranges", runtime.Str("ranges"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dv, err := buildDropdownBody(runtime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
data, err := runtime.CallAPITyped("PUT", dataValidationSheetPath(token, runtime.Str("sheet-id")), nil,
|
|
map[string]interface{}{
|
|
"ranges": ranges,
|
|
"dataValidationType": "list",
|
|
"dataValidation": dv,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
runtime.Out(data, nil)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// SheetGetDropdown queries dropdown list settings for a range.
|
|
var SheetGetDropdown = common.Shortcut{
|
|
Service: "sheets",
|
|
Command: "+get-dropdown",
|
|
Description: "Get dropdown list settings for a range",
|
|
Risk: "read",
|
|
Scopes: []string{"sheets:spreadsheet:read"},
|
|
AuthTypes: []string{"user", "bot"},
|
|
Flags: []common.Flag{
|
|
{Name: "url", Desc: "spreadsheet URL"},
|
|
{Name: "spreadsheet-token", Desc: "spreadsheet token"},
|
|
{Name: "range", Desc: "cell range (<sheetId>!A2:A100)", Required: true},
|
|
},
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
if _, err := validateDropdownToken(runtime); err != nil {
|
|
return err
|
|
}
|
|
if _, _, ok := splitSheetRange(runtime.Str("range")); !ok {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--range must be a fully qualified range with sheet ID prefix (e.g. <sheetId>!A2:A100)").WithParam("--range")
|
|
}
|
|
return nil
|
|
},
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
token, _ := validateDropdownToken(runtime)
|
|
return common.NewDryRunAPI().
|
|
GET("/open-apis/sheets/v2/spreadsheets/:token/dataValidation?range=:range&dataValidationType=list").
|
|
Set("token", token).Set("range", runtime.Str("range"))
|
|
},
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
token, _ := validateDropdownToken(runtime)
|
|
data, err := runtime.CallAPITyped("GET", dataValidationBasePath(token),
|
|
map[string]interface{}{
|
|
"range": runtime.Str("range"),
|
|
"dataValidationType": "list",
|
|
}, nil,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
runtime.Out(data, nil)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// SheetDeleteDropdown deletes dropdown list settings from given ranges.
|
|
var SheetDeleteDropdown = common.Shortcut{
|
|
Service: "sheets",
|
|
Command: "+delete-dropdown",
|
|
Description: "Delete dropdown list from cell ranges",
|
|
Risk: "write",
|
|
Scopes: []string{"sheets:spreadsheet:write_only", "sheets:spreadsheet:read"},
|
|
AuthTypes: []string{"user", "bot"},
|
|
Flags: []common.Flag{
|
|
{Name: "url", Desc: "spreadsheet URL"},
|
|
{Name: "spreadsheet-token", Desc: "spreadsheet token"},
|
|
{Name: "ranges", Desc: `ranges as JSON array (e.g. '["sheetId!A2:A100"]'), max 100 ranges`, Required: true},
|
|
},
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
if _, err := validateDropdownToken(runtime); err != nil {
|
|
return err
|
|
}
|
|
_, err := validateRangesFlag(runtime)
|
|
return err
|
|
},
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
token, _ := validateDropdownToken(runtime)
|
|
ranges, _ := parseJSONStringArray("ranges", runtime.Str("ranges"))
|
|
dvRanges := make([]interface{}, 0, len(ranges))
|
|
for _, r := range ranges {
|
|
dvRanges = append(dvRanges, map[string]interface{}{"range": r})
|
|
}
|
|
return common.NewDryRunAPI().
|
|
DELETE("/open-apis/sheets/v2/spreadsheets/:token/dataValidation").
|
|
Body(map[string]interface{}{
|
|
"dataValidationRanges": dvRanges,
|
|
}).
|
|
Set("token", token)
|
|
},
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
token, _ := validateDropdownToken(runtime)
|
|
ranges, err := parseJSONStringArray("ranges", runtime.Str("ranges"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dvRanges := make([]interface{}, 0, len(ranges))
|
|
for _, r := range ranges {
|
|
dvRanges = append(dvRanges, map[string]interface{}{"range": r})
|
|
}
|
|
|
|
data, err := runtime.CallAPITyped("DELETE", dataValidationBasePath(token), nil,
|
|
map[string]interface{}{
|
|
"dataValidationRanges": dvRanges,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
runtime.Out(data, nil)
|
|
return nil
|
|
},
|
|
}
|