Skip to content

feat(base): auto prune 5 empty rows upon base creation#274

Open
Scofy0123 wants to merge 1 commit intolarksuite:mainfrom
Scofy0123:feat/auto-clean-base-records
Open

feat(base): auto prune 5 empty rows upon base creation#274
Scofy0123 wants to merge 1 commit intolarksuite:mainfrom
Scofy0123:feat/auto-clean-base-records

Conversation

@Scofy0123
Copy link
Copy Markdown

@Scofy0123 Scofy0123 commented Apr 6, 2026

Motivation / 背景动因

In scenarios where lark-cli is utilized within an automated pipeline (e.g., CI/CD, data scraping, syncing scripts), creating a new Base is explicitly intended for programmatic data ingestion. However, the Feishu OpenAPI inherently initializes 5 empty records in the default table of a newly created Base.
While this is helpful for human users via the GUI to understand where to input data, it introduces significant side-effects and deterministic data order issues for scripts and developers writing to an unexpectedly "dirty" table.

在自动化的工作流场景中(如爬虫同步、数据自动化录入等),开发者通过 lark-cli 创建一个全新的多维表格(Base),其唯一目的就是提供一个结构化的、干净的数据底座以便后续程序化定点写入数据。然而,受限于飞书平台层的默认行为,新建的 Base 内部第一张默认表会自动生成 5 行空数据。这对机器脚本而言是非常破坏性的(会导致新插数据行号偏移或数据完整层面的错乱)。

Proposed Solution / 解决方案

This PR implements an orchestrating "cleanup" phase internally attached to the executeBaseCreate workflow:

  1. Adds a --keep-empty-rows boolean flag (defaults to false), maintaining zero-config convenience while pruning records by default.
  2. Upon successful Base creation, the CLI will transparently retrieve the default table and purge up to 10 empty records initialization rows.
  3. Added the corresponding required scopes (bitable:app.table:read & bitable:app.table.record:delete) into the base create command definitions to ensure seamless permissions.
  4. Any errors encountered during the cleanup phase are gracefully swallowed to guarantee that the primary create command remains successful regardless of unexpected permission restrictions.

本 PR 包含以下优化,主要是在 executeBaseCreate 中挂载了尽力而为的静默后置清理操作:

  1. lark-cli base +base-create 命令追加 --keep-empty-rows 退出选项(默认 false,即遵循“机器建表必须干净”的潜规则)。
  2. 在原先的创建 API (POST) 成功且返回 Token 后,利用已封装完成的底层方法,找到产生的首张表执行 GET records 后遍历 DELETE 以实现秒级“自清理”。
  3. 补充了默认命令声明所需的读表/删记录的底层安全 Scope 集合。
  4. 在该后处理逻辑中进行了极高容忍度的容错保护,任意异常均绝不会阻塞“建表成功”这一核心响应。

Type: feat (DX improvement)

Summary by CodeRabbit

  • New Features

    • Added a keep-empty-rows flag (default: false) to control whether default placeholder rows are preserved after creating a new base.
  • Bug Fixes / Improvements

    • By default, newly created bases now automatically prune placeholder empty rows.
    • Creation proceeds even if cleanup encounters errors.
    • Creation output now includes a count of pruned rows when any were removed.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 6, 2026

CLA assistant check
All committers have signed the CLA.

@github-actions github-actions bot added domain/base PR touches the base domain size/M Single-domain feat or fix with limited business impact labels Apr 6, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds conditional post-create cleanup that deletes default empty records from a new base's first table unless keep-empty-rows is set; expands scopes to allow table listing and record deletion and adds a keep-empty-rows CLI flag.

Changes

Cohort / File(s) Summary
Authorization & CLI flag
shortcuts/base/base_create.go
Expanded Scopes from []string{"base:app:create"} to []string{"base:app:create", "bitable:app.table:read", "bitable:app.table.record:delete"}. Added boolean flag keep-empty-rows (Type: "bool", Default: "false").
Post-create empty-row pruning
shortcuts/base/base_ops.go
executeBaseCreate now, when keep-empty-rows is false, derives baseToken, lists up to 10 tables, fetches up to 10 records from the first table, deletes records whose fields map is empty and record_id is a string, counts deletions, ignores list/fetch/delete errors, and includes "_pruned_records" in output only if >0.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI/User
    participant CreateAPI as BaseCreate API
    participant TableAPI as Table List API
    participant RecordAPI as Record Fetch API
    participant DeleteAPI as Record Delete API

    CLI->>CreateAPI: create base (keep-empty-rows flag)
    CreateAPI-->>CLI: returns base creation data (may include base_token)

    alt keep-empty-rows == false
        CreateAPI->>TableAPI: list tables using base_token
        TableAPI-->>CreateAPI: returns table list
        CreateAPI->>RecordAPI: fetch records from first table
        RecordAPI-->>CreateAPI: returns record list
        loop for each record with empty fields
            CreateAPI->>DeleteAPI: DELETE record by id
            DeleteAPI-->>CreateAPI: deletion response
        end
        CreateAPI-->>CreateAPI: aggregate _pruned_records count
    end

    CreateAPI-->>CLI: output created base data (includes _pruned_records if >0)
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through tables with gentle paws,
A flag to keep or prune the empty claws.
I nudged blank rows out of the gate,
Tallied tidy hops — a small clean slate.
Off I bounced to find another crate.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: auto-pruning 5 empty rows upon base creation, matching the primary objective of the PR.
Description check ✅ Passed The description comprehensively covers motivation, proposed solution with implementation details, and implications, with bilingual clarity. It addresses all key aspects of the changes effectively.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 6, 2026

Greptile Summary

This PR adds an auto-prune phase to executeBaseCreate that silently deletes the 5 default empty rows Feishu inserts into a newly created base, controlled by a new --keep-empty-rows flag. The emptiness guard (len(fieldsMap) == 0) from the prior review round has been added correctly. However, two prior-round P1 findings remain unresolved: the added scope strings (bitable:app.table:read, bitable:app.table.record:delete) still use a non-standard namespace — every other shortcut in this package uses the base:* format (e.g. "base:table:read", "base:record:delete") — and those scopes are still listed unconditionally even when --keep-empty-rows=true bypasses the cleanup entirely.

Confidence Score: 4/5

Not safe to merge — wrong scope identifier format will cause spurious permission errors for all users of +base-create

The prior-round finding about incorrect bitable:* scope strings (correct format is base:* per every other shortcut in this package, e.g. base:table:read and base:record:delete) remains unaddressed and will break the command for all users regardless of --keep-empty-rows. The emptiness-guard fix is a positive improvement, but the scope issue is a P1 that must be resolved first.

shortcuts/base/base_create.go — scope string identifiers need correction before merge

Important Files Changed

Filename Overview
shortcuts/base/base_create.go Adds --keep-empty-rows flag; scope strings still use incorrect bitable:* format instead of base:*, and are unconditionally declared regardless of flag value
shortcuts/base/base_ops.go Cleanup loop correctly guards on empty fields before deleting; _pruned_records emitted as top-level key but only conditionally and retains underscore prefix

Sequence Diagram

sequenceDiagram
    participant CLI as CLI (executeBaseCreate)
    participant LarkAPI as Lark API

    CLI->>LarkAPI: POST /open-apis/base/v3/bases
    LarkAPI-->>CLI: base data (incl. base_token)

    alt --keep-empty-rows is false (default)
        CLI->>CLI: extract base_token from response

        CLI->>LarkAPI: GET /bases/{base_token}/tables
        LarkAPI-->>CLI: tables list

        CLI->>CLI: select default table (tables[0])

        CLI->>LarkAPI: GET /bases/{base_token}/tables/{table_id}/records?limit=10
        LarkAPI-->>CLI: records list

        loop for each record where len(fields) == 0
            CLI->>LarkAPI: DELETE /bases/{base_token}/tables/{table_id}/records/{record_id}
            LarkAPI-->>CLI: delete confirmed
            CLI->>CLI: increment deletedTotal
        end
    end

    alt deletedTotal > 0
        CLI->>CLI: output {base, created:true, _pruned_records:N}
    else
        CLI->>CLI: output {base, created:true}
    end
Loading

Reviews (5): Last reviewed commit: "feat(base): auto prune 5 empty rows upon..." | Re-trigger Greptile

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shortcuts/base/base_create.go`:
- Around line 17-23: The command currently always requests destructive scopes
("bitable:app.table:read" and "bitable:app.table.record:delete") even when the
--keep-empty-rows flag is used; update the scope selection so those scopes are
only added when cleanup is enabled. Modify the code that defines Scopes (and/or
the command bootstrap where executeBaseCreate reads flags) to check the
"keep-empty-rows" flag and append the table-read/record-delete scopes only when
keep-empty-rows is false (or instead perform a separate auth request for cleanup
inside executeBaseCreate when pruning is performed). Ensure references to the
flag name "keep-empty-rows" and the function executeBaseCreate are used to
locate and gate the scope addition.

In `@shortcuts/base/base_ops.go`:
- Around line 108-116: The delete loop currently deletes up to 10 returned
records unconditionally; modify the loop that iterates rawItems (where
baseV3Call is used to fetch records) to call a new or existing predicate
function (e.g., recordLooksEmpty(record map[string]interface{})) and only issue
the DELETE via baseV3Call when that predicate returns true, and enforce a hard
cap to stop after deleting 5 rows; ensure you check item types
(map[string]interface{}) as before and increment a deletedCount only when a
successful delete occurs so the loop exits when deletedCount reaches 5.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 89b2031c-116c-472b-88dd-3aafefac6d5d

📥 Commits

Reviewing files that changed from the base of the PR and between 0c77c95 and b2e60ee.

📒 Files selected for processing (2)
  • shortcuts/base/base_create.go
  • shortcuts/base/base_ops.go

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
shortcuts/base/base_ops.go (1)

126-126: Internal metadata pollutes the API response output.

Adding _pruned_records directly to the data map (which holds the API response) means this internal metadata will appear in the user-facing JSON output at line 133 under "base". Consider storing it at the top level of the output map instead, or omitting it from the final output.

♻️ Proposed fix to separate internal metadata from API response
-						data["_pruned_records"] = deletedCount
+						// Store pruned count at output level, not inside API response

Then modify the output at line 133:

output := map[string]interface{}{"base": data, "created": true}
if prunedCount, ok := /* track deletedCount separately */; ok && prunedCount > 0 {
    output["_pruned_records"] = prunedCount
}
runtime.Out(output, nil)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/base/base_ops.go` at line 126, The code currently injects internal
metadata into the user-facing response by setting data["_pruned_records"] =
deletedCount; remove that assignment so the API payload in the "data" map stays
clean, keep tracking deletedCount as a separate local variable, then build a
top-level output map (e.g., output := map[string]interface{}{"base": data,
"created": true}) and, if deletedCount > 0, add output["_pruned_records"] =
deletedCount before calling runtime.Out(output, nil); update any references to
"data", "deletedCount", "output", and the runtime.Out(...) call accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@shortcuts/base/base_ops.go`:
- Line 126: The code currently injects internal metadata into the user-facing
response by setting data["_pruned_records"] = deletedCount; remove that
assignment so the API payload in the "data" map stays clean, keep tracking
deletedCount as a separate local variable, then build a top-level output map
(e.g., output := map[string]interface{}{"base": data, "created": true}) and, if
deletedCount > 0, add output["_pruned_records"] = deletedCount before calling
runtime.Out(output, nil); update any references to "data", "deletedCount",
"output", and the runtime.Out(...) call accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e747ac8c-b6b0-487e-9b9f-19079c7307b4

📥 Commits

Reviewing files that changed from the base of the PR and between b2e60ee and ddf0f99.

📒 Files selected for processing (1)
  • shortcuts/base/base_ops.go

@Scofy0123 Scofy0123 force-pushed the feat/auto-clean-base-records branch from 10f503e to 205145c Compare April 6, 2026 07:15
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
shortcuts/base/base_ops.go (1)

108-109: Consider guarding against empty table ID.

If tableID(tables[0]) returns an empty string (e.g., malformed table entry), the API path becomes /bases/.../tables//records, which would fail. While the error is swallowed, adding a guard avoids the unnecessary API call.

 			defaultTableID := tableID(tables[0])
+			if defaultTableID == "" {
+				goto outputResult
+			}
 			recordsData, err := baseV3Call(runtime, "GET", baseV3Path("bases", baseToken, "tables", defaultTableID, "records"), map[string]interface{}{"limit": 10}, nil)

Alternatively, wrap the remaining cleanup logic in if defaultTableID != "".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/base/base_ops.go` around lines 108 - 109, Guard against an empty
table ID before calling the API: obtain defaultTableID via tableID(tables[0])
and if it's empty return or skip the baseV3Call to avoid forming a path like
"/tables//records"; update the logic around defaultTableID in the function
containing baseV3Call/baseV3Path (check defaultTableID != "" and only call
baseV3Call or execute the subsequent cleanup when non-empty) so the API call is
not made with a malformed path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@shortcuts/base/base_ops.go`:
- Around line 108-109: Guard against an empty table ID before calling the API:
obtain defaultTableID via tableID(tables[0]) and if it's empty return or skip
the baseV3Call to avoid forming a path like "/tables//records"; update the logic
around defaultTableID in the function containing baseV3Call/baseV3Path (check
defaultTableID != "" and only call baseV3Call or execute the subsequent cleanup
when non-empty) so the API call is not made with a malformed path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cde45eb1-1fe9-46f4-8257-bdac4b1153f2

📥 Commits

Reviewing files that changed from the base of the PR and between 10f503e and 205145c.

📒 Files selected for processing (2)
  • shortcuts/base/base_create.go
  • shortcuts/base/base_ops.go
✅ Files skipped from review due to trivial changes (1)
  • shortcuts/base/base_create.go

- Prunes the 5 empty rows automatically generated by Feishu during base creation.

- Retains rows if --keep-empty-rows is specified.

- Adds emptiness guard to prevent deleting user records during prune.

- Ensures API response structure is unmutated.
@Scofy0123 Scofy0123 force-pushed the feat/auto-clean-base-records branch from 205145c to ead5ec5 Compare April 6, 2026 07:40
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
shortcuts/base/base_ops.go (1)

112-125: ⚠️ Potential issue | 🟠 Major

Enforce a hard stop after deleting 5 rows.

Line 112 iterates all fetched records (limit 10), and deletions continue as long as rows look empty. This can prune more than the intended default five rows.

💡 Proposed fix
 					if rawItems, ok := recordsData["items"].([]interface{}); ok {
 						for _, item := range rawItems {
+							if deletedTotal >= 5 {
+								break
+							}
 							if recMap, ok := item.(map[string]interface{}); ok {
 								// Emptiness guard: only delete if 'fields' is strictly empty (no values filled yet)
 								fieldsMap, _ := recMap["fields"].(map[string]interface{})
 								if len(fieldsMap) == 0 {
 									if recID, ok := recMap["record_id"].(string); ok {
 										_, delErr := baseV3Call(runtime, "DELETE", baseV3Path("bases", baseToken, "tables", defaultTableID, "records", recID), nil, nil)
 										if delErr == nil {
 											deletedTotal++
 										}
 									}
 								}
 							}
 						}
 					}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/base/base_ops.go` around lines 112 - 125, The loop over rawItems in
base_ops.go can delete more than five rows; enforce a hard cap by checking
deletedTotal against the limit (5) and breaking out once reached: inside the
loop that iterates rawItems (the block that checks fieldsMap and calls
baseV3Call to delete a record), add a guard so if deletedTotal >= 5 you stop
further deletions (break out of the loop or return from the enclosing function
as appropriate), ensuring the increment of deletedTotal remains where it is
after a successful delete; reference symbols: rawItems, deletedTotal, fieldsMap,
baseV3Call, baseV3Path, defaultTableID.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shortcuts/base/base_ops.go`:
- Around line 115-117: The current guard uses a failed type assertion
(fieldsMap, _ := recMap["fields"].(map[string]interface{})) so nil is treated as
empty; change the check to require a successful assertion and only treat truly
empty maps as deletable—i.e., perform fieldsMap, ok :=
recMap["fields"].(map[string]interface{}) and only enter the deletion branch
when ok is true and len(fieldsMap) == 0; reference the existing recMap,
fieldsMap and the deletion branch that uses recID/record_id to locate the
record.

---

Duplicate comments:
In `@shortcuts/base/base_ops.go`:
- Around line 112-125: The loop over rawItems in base_ops.go can delete more
than five rows; enforce a hard cap by checking deletedTotal against the limit
(5) and breaking out once reached: inside the loop that iterates rawItems (the
block that checks fieldsMap and calls baseV3Call to delete a record), add a
guard so if deletedTotal >= 5 you stop further deletions (break out of the loop
or return from the enclosing function as appropriate), ensuring the increment of
deletedTotal remains where it is after a successful delete; reference symbols:
rawItems, deletedTotal, fieldsMap, baseV3Call, baseV3Path, defaultTableID.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fcab5366-91c4-4af6-844d-7be01b004917

📥 Commits

Reviewing files that changed from the base of the PR and between 205145c and ead5ec5.

📒 Files selected for processing (2)
  • shortcuts/base/base_create.go
  • shortcuts/base/base_ops.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • shortcuts/base/base_create.go

@kongenpei
Copy link
Copy Markdown
Collaborator

感谢您的建议。我们正在研究更优的解法,准备从接口侧支持这个需求,有进展会及时同步出来。

Thanks for the feedback. We are working on a better approach and plan to support this at the API level. We will share updates promptly as progress is made.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain/base PR touches the base domain size/M Single-domain feat or fix with limited business impact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants