Files
larksuite-cli/shortcuts/base/base_ops.go
zgz2048 077b5e7180 feat: configure initial base table schema (#1377)
* feat: configure initial base table schema

* fix: add base create table scopes
2026-06-10 15:47:33 +08:00

271 lines
9.7 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package base
import (
"context"
"fmt"
"strings"
"time"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/shortcuts/common"
)
const baseCreateHint = "Tip: Base created the platform default first-table schema. To configure the initial table schema during +base-create, pass both --table-name and --fields."
var baseCreateDefaultTableDeleteDelay = time.Second
func dryRunBaseGet(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
return common.NewDryRunAPI().
GET("/open-apis/base/v3/bases/:base_token").
Set("base_token", runtime.Str("base-token"))
}
func dryRunBaseCopy(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
d := common.NewDryRunAPI().
POST("/open-apis/base/v3/bases/:base_token/copy").
Body(buildBaseCopyBody(runtime)).
Set("base_token", runtime.Str("base-token"))
if runtime.IsBot() {
d.Desc("After Base copy succeeds in bot mode, the CLI will also try to grant the current CLI user full_access (可管理权限) on the new Base.")
}
return d
}
func dryRunBaseCreate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
d := common.NewDryRunAPI()
if runtime.IsBot() {
d.Desc("After Base creation succeeds in bot mode, the CLI will also try to grant the current CLI user full_access (可管理权限) on the new Base.")
}
d.
POST("/open-apis/base/v3/bases").
Body(buildBaseCreateBody(runtime))
hasFields := strings.TrimSpace(runtime.Str("fields")) != ""
hasTableName := strings.TrimSpace(runtime.Str("table-name")) != ""
if hasFields {
d.GET("/open-apis/base/v3/bases/:created_base_token/tables").
Params(map[string]interface{}{"offset": 0, "limit": 100}).
Desc("If the create response does not include the default table ID, the CLI lists tables to find it.").
Set("created_base_token", "<created_base_token>")
d.POST("/open-apis/base/v3/bases/:created_base_token/tables").
Body(dryRunTableCreateBody(runtime, baseCreateFirstTableName(runtime))).
Desc("Create a replacement first table with --fields in the create-table body.")
d.DELETE("/open-apis/base/v3/bases/:created_base_token/tables/:default_table_id").
Desc("After a 1s wait, delete the default table created with the Base.").
Set("default_table_id", "<default_table_id>")
} else if hasTableName {
d.GET("/open-apis/base/v3/bases/:created_base_token/tables").
Params(map[string]interface{}{"offset": 0, "limit": 100}).
Desc("If the create response does not include the default table ID, the CLI lists tables to find it.").
Set("created_base_token", "<created_base_token>")
d.PATCH("/open-apis/base/v3/bases/:created_base_token/tables/:default_table_id").
Body(map[string]interface{}{"name": baseCreateFirstTableName(runtime)}).
Desc("Rename the default first table when only --table-name is provided.").
Set("default_table_id", "<default_table_id>")
}
return d
}
func validateBaseCreate(runtime *common.RuntimeContext) error {
return nil
}
func executeBaseGet(runtime *common.RuntimeContext) error {
data, err := baseV3Call(runtime, "GET", baseV3Path("bases", runtime.Str("base-token")), nil, nil)
if err != nil {
return err
}
runtime.Out(map[string]interface{}{"base": data}, nil)
return nil
}
func executeBaseCopy(runtime *common.RuntimeContext) error {
data, err := baseV3Call(runtime, "POST", baseV3Path("bases", runtime.Str("base-token"), "copy"), nil, buildBaseCopyBody(runtime))
if err != nil {
return err
}
out := map[string]interface{}{"base": data, "copied": true}
augmentBasePermissionGrant(runtime, out, data)
runtime.Out(out, nil)
return nil
}
func executeBaseCreate(runtime *common.RuntimeContext) error {
data, err := baseV3Call(runtime, "POST", baseV3Path("bases"), nil, buildBaseCreateBody(runtime))
if err != nil {
return err
}
out := map[string]interface{}{"base": data, "created": true}
if strings.TrimSpace(runtime.Str("fields")) != "" {
customTable, createdFields, defaultTableID, err := replaceBaseDefaultTable(runtime, data)
if err != nil {
return err
}
out["table"] = customTable
out["fields"] = createdFields
out["default_table_deleted"] = true
out["deleted_default_table_id"] = defaultTableID
} else if strings.TrimSpace(runtime.Str("table-name")) != "" {
renamedTable, defaultTableID, err := renameBaseDefaultTable(runtime, data)
if err != nil {
return err
}
out["table"] = renamedTable
out["default_table_renamed"] = true
out["renamed_default_table_id"] = defaultTableID
} else {
fmt.Fprintln(runtime.IO().ErrOut, baseCreateHint)
}
augmentBasePermissionGrant(runtime, out, data)
runtime.Out(out, nil)
return nil
}
func buildBaseCopyBody(runtime *common.RuntimeContext) map[string]interface{} {
body := map[string]interface{}{}
if name := strings.TrimSpace(runtime.Str("name")); name != "" {
body["name"] = name
}
if folderToken := strings.TrimSpace(runtime.Str("folder-token")); folderToken != "" {
body["folder_token"] = folderToken
}
if runtime.Bool("without-content") {
body["without_content"] = true
}
if timeZone := strings.TrimSpace(runtime.Str("time-zone")); timeZone != "" {
body["time_zone"] = timeZone
}
return body
}
func buildBaseCreateBody(runtime *common.RuntimeContext) map[string]interface{} {
body := map[string]interface{}{"name": runtime.Str("name")}
if folderToken := strings.TrimSpace(runtime.Str("folder-token")); folderToken != "" {
body["folder_token"] = folderToken
}
if timeZone := strings.TrimSpace(runtime.Str("time-zone")); timeZone != "" {
body["time_zone"] = timeZone
}
return body
}
func augmentBasePermissionGrant(runtime *common.RuntimeContext, out, base map[string]interface{}) {
if grant := common.AutoGrantCurrentUserDrivePermission(runtime, extractBasePermissionToken(base), "bitable"); grant != nil {
out["permission_grant"] = grant
}
}
func extractBasePermissionToken(base map[string]interface{}) string {
for _, key := range []string{"base_token", "app_token"} {
if token := strings.TrimSpace(common.GetString(base, key)); token != "" {
return token
}
}
return ""
}
func replaceBaseDefaultTable(runtime *common.RuntimeContext, base map[string]interface{}) (map[string]interface{}, []interface{}, string, error) {
baseToken := extractBasePermissionToken(base)
if baseToken == "" {
return nil, nil, "", errs.NewInternalError(errs.SubtypeInvalidResponse, "+base-create --fields could not find base_token/app_token in create response")
}
defaultTableID, err := findCreatedBaseDefaultTableID(runtime, baseToken, base, "--fields")
if err != nil {
return nil, nil, "", err
}
tableBody, err := buildTableCreateBody(runtime, newParseCtx(runtime), baseCreateFirstTableName(runtime))
if err != nil {
return nil, nil, "", err
}
customTable, err := baseV3Call(runtime, "POST", baseV3Path("bases", baseToken, "tables"), nil, tableBody)
if err != nil {
return nil, nil, "", err
}
customTableID := tableID(customTable)
if customTableID == "" {
return nil, nil, "", errs.NewInternalError(errs.SubtypeInvalidResponse, "+base-create --fields could not find table_id/id in replacement table create response")
}
createdFields := []interface{}{}
if fields, ok := customTable["fields"].([]interface{}); ok {
createdFields = fields
}
time.Sleep(baseCreateDefaultTableDeleteDelay)
if _, err := baseV3Call(runtime, "DELETE", baseV3Path("bases", baseToken, "tables", defaultTableID), nil, nil); err != nil {
return nil, nil, "", err
}
return customTable, createdFields, defaultTableID, nil
}
func renameBaseDefaultTable(runtime *common.RuntimeContext, base map[string]interface{}) (map[string]interface{}, string, error) {
baseToken := extractBasePermissionToken(base)
if baseToken == "" {
return nil, "", errs.NewInternalError(errs.SubtypeInvalidResponse, "+base-create --table-name could not find base_token/app_token in create response")
}
defaultTableID, err := findCreatedBaseDefaultTableID(runtime, baseToken, base, "--table-name")
if err != nil {
return nil, "", err
}
renamedTable, err := baseV3Call(
runtime,
"PATCH",
baseV3Path("bases", baseToken, "tables", defaultTableID),
nil,
map[string]interface{}{"name": baseCreateFirstTableName(runtime)},
)
if err != nil {
return nil, "", err
}
return renamedTable, defaultTableID, nil
}
func baseCreateFirstTableName(runtime *common.RuntimeContext) string {
if name := strings.TrimSpace(runtime.Str("table-name")); name != "" {
return name
}
return "Table 1"
}
func findCreatedBaseDefaultTableID(runtime *common.RuntimeContext, baseToken string, base map[string]interface{}, flag string) (string, error) {
if tableIDValue := tableIDFromCreateBaseResponse(base); tableIDValue != "" {
return tableIDValue, nil
}
tables, _, err := listAllTables(runtime, baseToken, 0, 100)
if err != nil {
return "", err
}
if len(tables) == 0 {
return "", errs.NewInternalError(errs.SubtypeInvalidResponse, "+base-create "+flag+" could not find the default table created with the Base")
}
if id := tableID(tables[0]); id != "" {
return id, nil
}
return "", errs.NewInternalError(errs.SubtypeInvalidResponse, "+base-create "+flag+" could not find table_id/id in default table list response")
}
func tableIDFromCreateBaseResponse(base map[string]interface{}) string {
for _, key := range []string{"table_id", "default_table_id"} {
if id := strings.TrimSpace(common.GetString(base, key)); id != "" {
return id
}
}
for _, key := range []string{"table", "default_table"} {
if table, ok := base[key].(map[string]interface{}); ok {
if id := tableID(table); id != "" {
return id
}
}
}
for _, key := range []string{"tables", "default_tables"} {
if tables, ok := base[key].([]interface{}); ok && len(tables) > 0 {
if table, ok := tables[0].(map[string]interface{}); ok {
if id := tableID(table); id != "" {
return id
}
}
}
}
return ""
}