diff --git a/shortcuts/base/base_execute_test.go b/shortcuts/base/base_execute_test.go index 46ec996d..fb6fb01c 100644 --- a/shortcuts/base/base_execute_test.go +++ b/shortcuts/base/base_execute_test.go @@ -222,7 +222,7 @@ func TestBaseRecordExecuteUpsertUpdate(t *testing.T) { "data": map[string]interface{}{"record_id": "rec_x", "fields": map[string]interface{}{"Name": "Alice"}}, }, }) - if err := runShortcut(t, BaseRecordUpsert, []string{"+record-upsert", "--base-token", "app_x", "--table-id", "tbl_x", "--record-id", "rec_x", "--json", `{"fields":{"Name":"Alice"}}`}, factory, stdout); err != nil { + if err := runShortcut(t, BaseRecordUpsert, []string{"+record-upsert", "--base-token", "app_x", "--table-id", "tbl_x", "--record-id", "rec_x", "--json", `{"Name":"Alice"}`}, factory, stdout); err != nil { t.Fatalf("err=%v", err) } if got := stdout.String(); !strings.Contains(got, `"updated": true`) || !strings.Contains(got, `"rec_x"`) { @@ -544,7 +544,7 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { "data": map[string]interface{}{"record_id": "rec_new", "fields": map[string]interface{}{"Name": "Alice"}}, }, }) - if err := runShortcut(t, BaseRecordUpsert, []string{"+record-upsert", "--base-token", "app_x", "--table-id", "tbl_x", "--json", `{"fields":{"Name":"Alice"}}`}, factory, stdout); err != nil { + if err := runShortcut(t, BaseRecordUpsert, []string{"+record-upsert", "--base-token", "app_x", "--table-id", "tbl_x", "--json", `{"Name":"Alice"}`}, factory, stdout); err != nil { t.Fatalf("err=%v", err) } if got := stdout.String(); !strings.Contains(got, `"created": true`) || !strings.Contains(got, `"rec_new"`) { @@ -552,6 +552,17 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { } }) + t.Run("reject top-level fields wrapper", func(t *testing.T) { + factory, stdout, _ := newExecuteFactory(t) + err := runShortcut(t, BaseRecordUpsert, []string{"+record-upsert", "--base-token", "app_x", "--table-id", "tbl_x", "--json", `{"fields":{"Name":"Alice"}}`}, factory, stdout) + if err == nil || !strings.Contains(err.Error(), "direct record object") { + t.Fatalf("err=%v", err) + } + if got := stdout.String(); got != "" { + t.Fatalf("stdout=%s", got) + } + }) + t.Run("delete", func(t *testing.T) { factory, stdout, reg := newExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -587,7 +598,7 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields/fld_att", Body: map[string]interface{}{ "code": 0, - "data": map[string]interface{}{"id": "fld_att", "name": "附件", "type": "attachment"}, + "data": map[string]interface{}{"id": "fld_att", "name": "附件", "type": "attachment"}, }, }) reg.Register(&httpmock.Stub{ @@ -598,7 +609,7 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { "data": map[string]interface{}{ "record_id": "rec_x", "fields": map[string]interface{}{ - "附件": []interface{}{ + "附件": []interface{}{ map[string]interface{}{ "file_token": "existing_tok", "name": "existing.pdf", @@ -629,7 +640,7 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { "data": map[string]interface{}{ "record_id": "rec_x", "fields": map[string]interface{}{ - "附件": []interface{}{ + "附件": []interface{}{ map[string]interface{}{ "file_token": "existing_tok", "name": "existing.pdf", @@ -671,7 +682,7 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { } updateBody := string(updateStub.CapturedBody) - if !strings.Contains(updateBody, `"附件"`) || + if !strings.Contains(updateBody, `"附件"`) || !strings.Contains(updateBody, `"file_token":"existing_tok"`) || !strings.Contains(updateBody, `"name":"existing.pdf"`) || !strings.Contains(updateBody, `"size":2048`) || @@ -704,7 +715,7 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields/fld_status", Body: map[string]interface{}{ "code": 0, - "data": map[string]interface{}{"id": "fld_status", "name": "状态", "type": "text"}, + "data": map[string]interface{}{"id": "fld_status", "name": "状态", "type": "text"}, }, }) @@ -899,13 +910,13 @@ func TestBaseFieldExecuteSearchOptions(t *testing.T) { URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields/fld_amount/options", Body: map[string]interface{}{ "code": 0, - "data": map[string]interface{}{"options": []interface{}{map[string]interface{}{"id": "opt_1", "name": "已完成"}}, "total": 1}, + "data": map[string]interface{}{"options": []interface{}{map[string]interface{}{"id": "opt_1", "name": "已完成"}}, "total": 1}, }, }) - if err := runShortcut(t, BaseFieldSearchOptions, []string{"+field-search-options", "--base-token", "app_x", "--table-id", "tbl_x", "--field-id", "fld_amount", "--keyword", "已", "--limit", "10"}, factory, stdout); err != nil { + if err := runShortcut(t, BaseFieldSearchOptions, []string{"+field-search-options", "--base-token", "app_x", "--table-id", "tbl_x", "--field-id", "fld_amount", "--keyword", "å·²", "--limit", "10"}, factory, stdout); err != nil { t.Fatalf("err=%v", err) } - if got := stdout.String(); !strings.Contains(got, `"options"`) || !strings.Contains(got, `"已完成"`) { + if got := stdout.String(); !strings.Contains(got, `"options"`) || !strings.Contains(got, `"已完成"`) { t.Fatalf("stdout=%s", got) } } diff --git a/shortcuts/base/base_shortcuts_test.go b/shortcuts/base/base_shortcuts_test.go index a6f1c61d..28a2c3d8 100644 --- a/shortcuts/base/base_shortcuts_test.go +++ b/shortcuts/base/base_shortcuts_test.go @@ -244,6 +244,9 @@ func TestBaseRecordValidate(t *testing.T) { 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) } + if err := BaseRecordUpsert.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "table-id": "tbl_1", "json": `{"fields":{"Name":"A"}}`}, nil, nil)); err == nil || !strings.Contains(err.Error(), "direct record object") { + t.Fatalf("err=%v", err) + } if err := BaseRecordUpsert.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "table-id": "tbl_1", "json": "{"}, nil, nil)); err != nil { t.Fatalf("invalid record json should bypass CLI validate, err=%v", err) } diff --git a/shortcuts/base/record_ops.go b/shortcuts/base/record_ops.go index 280b1c58..fb7cbefc 100644 --- a/shortcuts/base/record_ops.go +++ b/shortcuts/base/record_ops.go @@ -75,6 +75,17 @@ func dryRunRecordHistoryList(_ context.Context, runtime *common.RuntimeContext) } func validateRecordJSON(runtime *common.RuntimeContext) error { + body, err := parseJSONObject(runtime.Str("json"), "json") + if err != nil { + // Keep invalid JSON handling on the execution path unchanged; only + // intercept the common top-level shape mistake here. + return nil + } + if len(body) == 1 { + if fields, ok := body["fields"].(map[string]interface{}); ok && len(fields) > 0 { + return common.FlagErrorf("--json for +record-upsert must be a direct record object, not a top-level \"fields\" wrapper; use '{\"Name\":\"Alice\"}' instead of '{\"fields\":{\"Name\":\"Alice\"}}'. If your real field name is literally \"fields\", use the field ID as the key.") + } + } return nil }