mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
feat(base): add record field filters (#327)
* feat(base): add record field filters * fix(base): align record field filter flags with OpenAPI params * fix: scope record dry-run field filters and align docs * docs(base): clarify record-list field_scope priority * refactor(base): remove field-id from record-get --------- Co-authored-by: zgz2048 <zhonggangzhi.tim@bytedance.com> Co-authored-by: kongenpei <kongenpei@users.noreply.github.com>
This commit is contained in:
@@ -63,12 +63,21 @@ func TestDryRunFieldOps(t *testing.T) {
|
||||
func TestDryRunRecordOps(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
listRT := newBaseTestRuntime(
|
||||
listRT := newBaseTestRuntimeWithArrays(
|
||||
map[string]string{"base-token": "app_x", "table-id": "tbl_1", "view-id": "viw_1"},
|
||||
map[string][]string{"field-id": {"Name", "Age"}},
|
||||
nil,
|
||||
map[string]int{"offset": -3, "limit": 500},
|
||||
)
|
||||
assertDryRunContains(t, dryRunRecordList(ctx, listRT), "GET /open-apis/base/v3/bases/app_x/tables/tbl_1/records", "offset=0", "limit=200", "view_id=viw_1")
|
||||
assertDryRunContains(t, dryRunRecordList(ctx, listRT), "GET /open-apis/base/v3/bases/app_x/tables/tbl_1/records", "offset=0", "limit=200", "view_id=viw_1", "field_id=Name", "field_id=Age")
|
||||
|
||||
commaFieldRT := newBaseTestRuntimeWithArrays(
|
||||
map[string]string{"base-token": "app_x", "table-id": "tbl_1"},
|
||||
map[string][]string{"field-id": {"A,B", "C"}},
|
||||
nil,
|
||||
map[string]int{"limit": 1},
|
||||
)
|
||||
assertDryRunContains(t, dryRunRecordList(ctx, commaFieldRT), "limit=1", "offset=0", "field_id=A%2CB", "field_id=C")
|
||||
|
||||
upsertCreateRT := newBaseTestRuntime(
|
||||
map[string]string{"base-token": "app_x", "table-id": "tbl_1", "json": `{"Name":"A"}`},
|
||||
|
||||
@@ -471,6 +471,52 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list with fields and view", func(t *testing.T) {
|
||||
factory, stdout, reg := newExecuteFactory(t)
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "field_id=Name&field_id=Age&limit=1&offset=0&view_id=vew_x",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{
|
||||
"fields": []interface{}{"Name", "Age"},
|
||||
"record_id_list": []interface{}{"rec_fields"},
|
||||
"data": []interface{}{[]interface{}{"Alice", 18}},
|
||||
"total": 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err := runShortcut(t, BaseRecordList, []string{"+record-list", "--base-token", "app_x", "--table-id", "tbl_x", "--view-id", "vew_x", "--limit", "1", "--field-id", "Name", "--field-id", "Age"}, factory, stdout); err != nil {
|
||||
t.Fatalf("err=%v", err)
|
||||
}
|
||||
if got := stdout.String(); !strings.Contains(got, `"rec_fields"`) || !strings.Contains(got, `"Alice"`) {
|
||||
t.Fatalf("stdout=%s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list with comma field", func(t *testing.T) {
|
||||
factory, stdout, reg := newExecuteFactory(t)
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "field_id=A%2CB&field_id=C&limit=1&offset=0",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{
|
||||
"fields": []interface{}{"A,B", "C"},
|
||||
"record_id_list": []interface{}{"rec_json_fields"},
|
||||
"data": []interface{}{[]interface{}{"value-1", "value-2"}},
|
||||
"total": 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err := runShortcut(t, BaseRecordList, []string{"+record-list", "--base-token", "app_x", "--table-id", "tbl_x", "--limit", "1", "--field-id", "A,B", "--field-id", "C"}, factory, stdout); err != nil {
|
||||
t.Fatalf("err=%v", err)
|
||||
}
|
||||
if got := stdout.String(); !strings.Contains(got, `"A,B"`) || !strings.Contains(got, `"rec_json_fields"`) {
|
||||
t.Fatalf("stdout=%s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list new shape", func(t *testing.T) {
|
||||
factory, stdout, reg := newExecuteFactory(t)
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -494,6 +540,22 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list legacy fields flag rejected", func(t *testing.T) {
|
||||
factory, stdout, _ := newExecuteFactory(t)
|
||||
err := runShortcut(t, BaseRecordList, []string{"+record-list", "--base-token", "app_x", "--table-id", "tbl_x", "--fields", "Name"}, factory, stdout)
|
||||
if err == nil || !strings.Contains(err.Error(), "unknown flag: --fields") {
|
||||
t.Fatalf("err=%v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list legacy fields flag rejected in dry-run", func(t *testing.T) {
|
||||
factory, stdout, _ := newExecuteFactory(t)
|
||||
err := runShortcut(t, BaseRecordList, []string{"+record-list", "--base-token", "app_x", "--table-id", "tbl_x", "--fields", "Name", "--dry-run"}, factory, stdout)
|
||||
if err == nil || !strings.Contains(err.Error(), "unknown flag: --fields") {
|
||||
t.Fatalf("err=%v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get", func(t *testing.T) {
|
||||
factory, stdout, reg := newExecuteFactory(t)
|
||||
reg.Register(&httpmock.Stub{
|
||||
|
||||
@@ -18,10 +18,17 @@ import (
|
||||
)
|
||||
|
||||
func newBaseTestRuntime(stringFlags map[string]string, boolFlags map[string]bool, intFlags map[string]int) *common.RuntimeContext {
|
||||
return newBaseTestRuntimeWithArrays(stringFlags, nil, boolFlags, intFlags)
|
||||
}
|
||||
|
||||
func newBaseTestRuntimeWithArrays(stringFlags map[string]string, stringArrayFlags map[string][]string, boolFlags map[string]bool, intFlags map[string]int) *common.RuntimeContext {
|
||||
cmd := &cobra.Command{Use: "test"}
|
||||
for name := range stringFlags {
|
||||
cmd.Flags().String(name, "", "")
|
||||
}
|
||||
for name := range stringArrayFlags {
|
||||
cmd.Flags().StringArray(name, nil, "")
|
||||
}
|
||||
for name := range boolFlags {
|
||||
cmd.Flags().Bool(name, false, "")
|
||||
}
|
||||
@@ -32,6 +39,11 @@ func newBaseTestRuntime(stringFlags map[string]string, boolFlags map[string]bool
|
||||
for name, value := range stringFlags {
|
||||
_ = cmd.Flags().Set(name, value)
|
||||
}
|
||||
for name, values := range stringArrayFlags {
|
||||
for _, value := range values {
|
||||
_ = cmd.Flags().Set(name, value)
|
||||
}
|
||||
}
|
||||
for name, value := range boolFlags {
|
||||
if value {
|
||||
_ = cmd.Flags().Set(name, "true")
|
||||
@@ -236,10 +248,10 @@ func TestBaseTableValidate(t *testing.T) {
|
||||
func TestBaseRecordValidate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
if BaseRecordList.Validate != nil {
|
||||
t.Fatalf("record list validate should be nil after removing --fields")
|
||||
t.Fatalf("record list validate should be nil for repeatable --field-id")
|
||||
}
|
||||
if BaseRecordGet.Validate != nil {
|
||||
t.Fatalf("record get validate should be nil after removing --fields")
|
||||
t.Fatalf("record get validate should be nil")
|
||||
}
|
||||
if err := BaseRecordUpsert.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "table-id": "tbl_1", "json": `{"Name":"A"}`}, nil, nil)); err != nil {
|
||||
t.Fatalf("upsert validate err=%v", err)
|
||||
|
||||
@@ -379,7 +379,18 @@ func baseV3Path(parts ...string) string {
|
||||
func baseV3Raw(runtime *common.RuntimeContext, method, path string, params map[string]interface{}, data interface{}) (map[string]interface{}, error) {
|
||||
queryParams := make(larkcore.QueryParams)
|
||||
for k, v := range params {
|
||||
queryParams.Set(k, fmt.Sprintf("%v", v))
|
||||
switch val := v.(type) {
|
||||
case []string:
|
||||
for _, item := range val {
|
||||
queryParams.Add(k, item)
|
||||
}
|
||||
case []interface{}:
|
||||
for _, item := range val {
|
||||
queryParams.Add(k, fmt.Sprintf("%v", item))
|
||||
}
|
||||
default:
|
||||
queryParams.Set(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
req := &larkcore.ApiReq{
|
||||
HttpMethod: strings.ToUpper(method),
|
||||
|
||||
@@ -19,6 +19,7 @@ var BaseRecordList = common.Shortcut{
|
||||
Flags: []common.Flag{
|
||||
baseTokenFlag(true),
|
||||
tableRefFlag(true),
|
||||
{Name: "field-id", Type: "string_array", Desc: "field ID or field name to include (repeatable)"},
|
||||
{Name: "view-id", Desc: "view ID"},
|
||||
{Name: "offset", Type: "int", Default: "0", Desc: "pagination offset"},
|
||||
{Name: "limit", Type: "int", Default: "100", Desc: "pagination size"},
|
||||
|
||||
@@ -5,6 +5,8 @@ package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
@@ -15,13 +17,18 @@ func dryRunRecordList(_ context.Context, runtime *common.RuntimeContext) *common
|
||||
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
|
||||
params := url.Values{}
|
||||
params.Set("offset", strconv.Itoa(offset))
|
||||
params.Set("limit", strconv.Itoa(limit))
|
||||
for _, field := range recordListFields(runtime) {
|
||||
params.Add("field_id", field)
|
||||
}
|
||||
if viewID := runtime.Str("view-id"); viewID != "" {
|
||||
params.Set("view_id", viewID)
|
||||
}
|
||||
path := "/open-apis/base/v3/bases/:base_token/tables/:table_id/records?" + params.Encode()
|
||||
return common.NewDryRunAPI().
|
||||
GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/records").
|
||||
Params(params).
|
||||
GET(path).
|
||||
Set("base_token", runtime.Str("base-token")).
|
||||
Set("table_id", baseTableID(runtime))
|
||||
}
|
||||
@@ -79,6 +86,10 @@ func validateRecordJSON(runtime *common.RuntimeContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func recordListFields(runtime *common.RuntimeContext) []string {
|
||||
return runtime.StrArray("field-id")
|
||||
}
|
||||
|
||||
func executeRecordList(runtime *common.RuntimeContext) error {
|
||||
offset := runtime.Int("offset")
|
||||
if offset < 0 {
|
||||
@@ -86,6 +97,10 @@ func executeRecordList(runtime *common.RuntimeContext) error {
|
||||
}
|
||||
limit := common.ParseIntBounded(runtime, "limit", 1, 200)
|
||||
params := map[string]interface{}{"offset": offset, "limit": limit}
|
||||
fields := recordListFields(runtime)
|
||||
if len(fields) > 0 {
|
||||
params["field_id"] = fields
|
||||
}
|
||||
if viewID := runtime.Str("view-id"); viewID != "" {
|
||||
params["view_id"] = viewID
|
||||
}
|
||||
|
||||
@@ -164,7 +164,8 @@ metadata:
|
||||
|
||||
- **Base token 口径统一**:统一使用 `--base-token`
|
||||
- **`+xxx-list` 调用纪律**:`+table-list / +field-list / +record-list / +view-list / +record-history-list / +role-list / +dashboard-list / +dashboard-block-list / +workflow-list` 禁止并发调用;批量执行时只能串行
|
||||
- **`+record-list` 分页规则**:`--limit` 最大 `200`。先拉首批并检查返回 `has_more`;仅当 `has_more=true` 且用户明确需要更多数据(如“全部导出/全量明细/继续下一页”)时再继续翻页。用户只要样例或前 N 条时,不要继续拉全量
|
||||
- **`+record-list` 分页与 limit**:`--limit` 最大 `200`。先拉首批并检查 `has_more`;仅当 `has_more=true` 且用户明确需要更多数据(如“全部导出/全量明细/继续下一页”)时再按 `offset` 递增翻页,禁止单次传超过 `200`
|
||||
- **记录读取字段筛选**:`+record-list` 支持重复传参 --field-id 做字段筛选
|
||||
- **字段可写性先判断**:存储字段才可写;公式 / lookup / 系统字段默认只读,写记录时应跳过
|
||||
- **公式能力要主动想到**:用户说“算一下”“生成标签”“判断是否异常”“跨表汇总”“按日期差预警”时,要先判断是否应该建公式字段,而不是只返回手工分析方案
|
||||
- **lookup 不是默认首选**:lookup 只在用户明确要求或确实更适合固定查找模型时使用;常规计算、跨表聚合和条件判断优先 formula
|
||||
|
||||
@@ -11,12 +11,6 @@ lark-cli base +record-get \
|
||||
--base-token app_xxx \
|
||||
--table-id tbl_xxx \
|
||||
--record-id rec_xxx
|
||||
|
||||
lark-cli base +record-get \
|
||||
--base-token app_xxx \
|
||||
--table-id tbl_xxx \
|
||||
--record-id rec_xxx \
|
||||
--fields 项目名称,状态
|
||||
```
|
||||
|
||||
## 参数
|
||||
@@ -26,7 +20,6 @@ lark-cli base +record-get \
|
||||
| `--base-token <token>` | 是 | Base Token |
|
||||
| `--table-id <id_or_name>` | 是 | 表 ID 或表名 |
|
||||
| `--record-id <id>` | 是 | 记录 ID |
|
||||
| `--fields <csv_or_json>` | 否 | 字段名 CSV,或 JSON 字符串数组 |
|
||||
|
||||
## API 入参详情
|
||||
|
||||
@@ -38,8 +31,7 @@ GET /open-apis/base/v3/bases/:base_token/tables/:table_id/records/:record_id
|
||||
|
||||
## 返回重点
|
||||
|
||||
- 返回 `record` 和 `raw`。
|
||||
- `record` 是裁剪后的单条结果;`raw` 保留接口完整响应。
|
||||
- 成功时直接返回接口 `data` 字段内容。
|
||||
|
||||
## 参考
|
||||
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
|
||||
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
|
||||
分页列出一张表里的记录;可按视图过滤。
|
||||
分页列出一张表里的记录;可按视图过滤,也可按字段裁剪返回列。
|
||||
|
||||
## 返回关键字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `has_more` | boolean | 是否还有下一页数据;`true` 表示可继续翻页,`false` 表示已到末页 |
|
||||
| `query_context.record_scope` | string | 记录范围:`all_records`(全表)或 `view_filtered_records`(按视图过滤) |
|
||||
| `query_context.field_scope` | string | 字段范围:`selected_fields`(显式传 `--field-id`)/ `view_visible_fields`(未传 `--field-id` 且按视图可见字段)/ `all_fields`(未传 `--field-id` 且无视图限制) |
|
||||
|
||||
## 字段返回优先级
|
||||
|
||||
- `query_context.field_scope` 的优先级为:`selected_fields`(explicit `--field-id`) > `view_visible_fields`(view visible fields) > `all_fields`(table all fields)。
|
||||
|
||||
## 按需翻页规则
|
||||
|
||||
@@ -33,6 +39,8 @@ lark-cli base +record-list \
|
||||
--base-token app_xxx \
|
||||
--table-id tbl_xxx \
|
||||
--view-id viw_xxx \
|
||||
--field-id fld_status \
|
||||
--field-id 项目名称 \
|
||||
--offset 0 \
|
||||
--limit 50
|
||||
```
|
||||
@@ -44,6 +52,7 @@ lark-cli base +record-list \
|
||||
| `--base-token <token>` | 是 | Base Token |
|
||||
| `--table-id <id_or_name>` | 是 | 表 ID 或表名 |
|
||||
| `--view-id <id>` | 否 | 视图 ID;传入后只读该视图结果 |
|
||||
| `--field-id <id_or_name>` | 否 | 字段 ID 或字段名;可重复传入多个 `--field-id` 裁剪返回列 |
|
||||
| `--offset <n>` | 否 | 分页偏移,默认 `0` |
|
||||
| `--limit <n>` | 否 | 分页大小,默认 `100`,范围 `1-200`(最大 `200`,超过会报错) |
|
||||
|
||||
@@ -55,7 +64,7 @@ lark-cli base +record-list \
|
||||
GET /open-apis/base/v3/bases/:base_token/tables/:table_id/records
|
||||
```
|
||||
|
||||
- 查询参数会附带 `view_id / offset / limit`。
|
||||
- 查询参数会附带 `view_id / field_id(repeatable) / offset / limit`。
|
||||
|
||||
|
||||
## 坑点
|
||||
@@ -63,6 +72,7 @@ GET /open-apis/base/v3/bases/:base_token/tables/:table_id/records
|
||||
- ⚠️ `+record-list` 禁止并发调用;批量拉多个视图或多张表时必须串行。
|
||||
- ⚠️ `--limit` 最大 `200`,不要传超过 `200` 的值。
|
||||
- ⚠️ 分页时优先根据返回的 `has_more` 判断是否继续请求,不要盲目预拉全量数据。
|
||||
- ⚠️ `--field-id` 接受字段 ID 或字段名。
|
||||
- ⚠️ 复杂筛选优先落到视图里,再用 `--view-id` 读取。
|
||||
|
||||
## 参考
|
||||
|
||||
@@ -18,5 +18,6 @@ record 相关命令索引。
|
||||
|
||||
- 聚合页只保留目录职责;每个命令的详细说明请进入对应单命令文档。
|
||||
- 所有 `+xxx-list` 调用都必须串行执行;若要批量跑多个 list 请求,只能串行执行。
|
||||
- `+record-list` 支持重复传参 `--field-id` 做字段筛选。
|
||||
- 写记录 JSON 前优先阅读 [lark-base-shortcut-record-value.md](lark-base-shortcut-record-value.md)。
|
||||
- 本地文件写入附件字段时,必须使用 `+record-upload-attachment`。
|
||||
|
||||
Reference in New Issue
Block a user