diff --git a/shortcuts/base/base_dryrun_ops_test.go b/shortcuts/base/base_dryrun_ops_test.go index 3898826b..2c03148d 100644 --- a/shortcuts/base/base_dryrun_ops_test.go +++ b/shortcuts/base/base_dryrun_ops_test.go @@ -218,3 +218,18 @@ func TestDryRunViewOps(t *testing.T) { assertDryRunContains(t, dryRunViewGetProperty(listRT, "a/b"), "GET /open-apis/base/v3/bases/app_x/tables/tbl_1/views/viw_1/a%2Fb") } + +func TestDryRunRecordListWithViewName(t *testing.T) { + ctx := context.Background() + rt := newBaseTestRuntime( + map[string]string{"base-token": "app_x", "table-id": "tbl_1", "view-id": "Main"}, + nil, + map[string]int{"offset": 0, "limit": 20}, + ) + assertDryRunContains( + t, + dryRunRecordList(ctx, rt), + "GET /open-apis/base/v3/bases/app_x/tables/tbl_1/records", + "view_id=%3Cresolved+from+view+name%3A+Main%3E", + ) +} diff --git a/shortcuts/base/base_execute_test.go b/shortcuts/base/base_execute_test.go index 34128a93..c87b1072 100644 --- a/shortcuts/base/base_execute_test.go +++ b/shortcuts/base/base_execute_test.go @@ -1045,3 +1045,122 @@ func TestBaseViewExecutePropertyGettersAndExtendedSetters(t *testing.T) { } }) } + +func TestBaseRecordExecuteListWithViewNameResolvesToViewID(t *testing.T) { + factory, stdout, reg := newExecuteFactory(t) + registerTokenStub(reg) + + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/views", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "views": []interface{}{ + map[string]interface{}{"view_id": "vew_x", "view_name": "Main"}, + }, + "total": 1, + }, + }, + }) + + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "view_id=vew_x", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "records": map[string]interface{}{ + "schema": []interface{}{"Name"}, + "record_ids": []interface{}{"rec_1"}, + "rows": []interface{}{[]interface{}{"Alpha"}}, + }, + "total": 1, + }, + }, + }) + + if err := runShortcut(t, BaseRecordList, []string{"+record-list", "--base-token", "app_x", "--table-id", "tbl_x", "--view-id", "Main", "--limit", "1"}, factory, stdout); err != nil { + t.Fatalf("err=%v", err) + } + if got := stdout.String(); !strings.Contains(got, "\"rec_1\"") { + t.Fatalf("stdout=%s", got) + } +} + +func TestBaseRecordResolveViewIDAcrossPages(t *testing.T) { + factory, stdout, reg := newExecuteFactory(t) + registerTokenStub(reg) + + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/views?limit=200&offset=0", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "views": []interface{}{}, + "total": 201, + }, + }, + }) + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/views?limit=200&offset=200", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "views": []interface{}{ + map[string]interface{}{"view_id": "vew_target", "view_name": "Target View"}, + }, + "total": 201, + }, + }, + }) + + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "view_id=vew_target", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "records": map[string]interface{}{ + "schema": []interface{}{"Name"}, + "record_ids": []interface{}{"rec_last"}, + "rows": []interface{}{[]interface{}{"Tail"}}, + }, + "total": 1, + }, + }, + }) + + if err := runShortcut(t, BaseRecordList, []string{"+record-list", "--base-token", "app_x", "--table-id", "tbl_x", "--view-id", "Target View", "--limit", "1"}, factory, stdout); err != nil { + t.Fatalf("err=%v", err) + } + if got := stdout.String(); !strings.Contains(got, "\"rec_last\"") { + t.Fatalf("stdout=%s", got) + } +} + +func TestBaseRecordResolveViewIDMissingCanonicalID(t *testing.T) { + factory, stdout, reg := newExecuteFactory(t) + registerTokenStub(reg) + + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/views", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "views": []interface{}{ + map[string]interface{}{"view_name": "BrokenView"}, + }, + "total": 1, + }, + }, + }) + + err := runShortcut(t, BaseRecordList, []string{"+record-list", "--base-token", "app_x", "--table-id", "tbl_x", "--view-id", "BrokenView", "--limit", "1"}, factory, stdout) + if err == nil || !strings.Contains(err.Error(), "has no canonical id") { + t.Fatalf("err=%v", err) + } +} diff --git a/shortcuts/base/record_ops.go b/shortcuts/base/record_ops.go index 280b1c58..aac6ff39 100644 --- a/shortcuts/base/record_ops.go +++ b/shortcuts/base/record_ops.go @@ -5,10 +5,18 @@ package base import ( "context" + "fmt" + "strings" "github.com/larksuite/cli/shortcuts/common" ) +const recordListViewResolvePageLimit = 200 + +func isViewIDRef(viewRef string) bool { + return strings.HasPrefix(viewRef, "vew_") || strings.HasPrefix(viewRef, "viw_") +} + func dryRunRecordList(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { offset := runtime.Int("offset") if offset < 0 { @@ -16,8 +24,12 @@ func dryRunRecordList(_ context.Context, runtime *common.RuntimeContext) *common } limit := common.ParseIntBounded(runtime, "limit", 1, 200) params := map[string]interface{}{"offset": offset, "limit": limit} - if viewID := runtime.Str("view-id"); viewID != "" { - params["view_id"] = viewID + if viewRef := runtime.Str("view-id"); viewRef != "" { + if isViewIDRef(viewRef) { + params["view_id"] = viewRef + } else { + params["view_id"] = fmt.Sprintf("", viewRef) + } } return common.NewDryRunAPI(). GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/records"). @@ -26,6 +38,43 @@ func dryRunRecordList(_ context.Context, runtime *common.RuntimeContext) *common Set("table_id", baseTableID(runtime)) } +func resolveRecordListViewID(runtime *common.RuntimeContext, viewRef string) (string, error) { + if viewRef == "" { + return "", nil + } + if isViewIDRef(viewRef) { + return viewRef, nil + } + + offset := 0 + for { + views, total, err := listAllViews(runtime, runtime.Str("base-token"), baseTableID(runtime), offset, recordListViewResolvePageLimit) + if err != nil { + return "", err + } + if view, err := resolveViewRef(views, viewRef); err == nil { + resolvedID := viewID(view) + if resolvedID == "" { + return "", fmt.Errorf("view %q has no canonical id", viewRef) + } + return resolvedID, nil + } + if len(views) == 0 { + if total > 0 && offset+recordListViewResolvePageLimit < total { + offset += recordListViewResolvePageLimit + continue + } + break + } + offset += len(views) + if total > 0 && offset >= total { + break + } + } + + return "", fmt.Errorf("view %q not found", viewRef) +} + func dryRunRecordGet(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/records/:record_id"). @@ -85,8 +134,12 @@ func executeRecordList(runtime *common.RuntimeContext) error { } limit := common.ParseIntBounded(runtime, "limit", 1, 200) params := map[string]interface{}{"offset": offset, "limit": limit} - if viewID := runtime.Str("view-id"); viewID != "" { - params["view_id"] = viewID + if viewRef := runtime.Str("view-id"); viewRef != "" { + resolvedViewID, err := resolveRecordListViewID(runtime, viewRef) + if err != nil { + return err + } + params["view_id"] = resolvedViewID } data, err := baseV3Call(runtime, "GET", baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "records"), params, nil) if err != nil {