Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions shortcuts/base/base_dryrun_ops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
}
119 changes: 119 additions & 0 deletions shortcuts/base/base_execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
61 changes: 57 additions & 4 deletions shortcuts/base/record_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,31 @@ 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 {
offset = 0
}
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("<resolved from view name: %s>", viewRef)
}
}
return common.NewDryRunAPI().
GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/records").
Expand All @@ -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").
Expand Down Expand Up @@ -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 {
Expand Down
Loading