diff --git a/CHANGELOG.md b/CHANGELOG.md index b461799..4473b72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +## v1.5.7 (2026-08-17) +- **低代码结果投影**: 动态 Action 可通过 `resultMode: data` 让 JavaScript `api.Call` 直接返回业务数据,并补齐 `ok/code/error`。 +- **继承一致性**: 结果投影读取完整的 Action 继承配置,子 Action 无需重复声明。 +- **兼容边界**: Go `Call`/`CallBy` 与未配置该选项的 JavaScript Action 均保持原有完整 `Result` 行为。 + ## v1.5.6 (2026-08-17) - **统一调用结果**: `Call` 与 `CallBy` 返回 `ok/statusCode/headers/data/code/error`,支持响应错误字段和成功码规则。 - **调用选项**: 新增 Token 验证、超时、配置覆盖与敏感/控制字段覆盖保护。 diff --git a/README.md b/README.md index d38b26f..21be0bf 100644 --- a/README.md +++ b/README.md @@ -80,5 +80,7 @@ Action 只使用 `filters: ["name"]` 这一种形式。每个 Filter 都会收 动态 Action 可通过 `extends` 继承另一个 Action;父配置先合并,子配置深度覆盖。继承在每次调用时解析,因此父 Action 热更新会立即作用于子 Action。循环继承或不存在的父 Action 会直接返回错误。 +面向 JavaScript 低代码调用时,Action 可配置 `resultMode: data`,使 `api.Call` 直接返回业务 `data`,并保留顶层 `ok/code/error`,避免 HTTP 状态和响应头干扰应用逻辑。该配置支持 `extends` 继承,只影响 JavaScript 导出;Go 的 `Call` 与 `CallBy` 始终返回完整 `Result`。 + --- 更多详情请参阅 [TEST.md](./TEST.md) 和 [CHANGELOG.md](./CHANGELOG.md)。 diff --git a/TEST.md b/TEST.md index e3f1e62..cb0c1f2 100644 --- a/TEST.md +++ b/TEST.md @@ -35,11 +35,15 @@ 验证 `extends` 深度继承、父 Action 热解析、循环和缺失父项报错,以及 `filters: []` 同时收到请求与响应事件。JavaScript Filter 的中文流片段按 UTF-8 字符串传递。 +### 8. JavaScript 业务结果投影 (`TestProjectDataResult`, `TestActionUsesInheritedDataResult`) + +验证 `resultMode: data` 仅向 JavaScript 返回业务数据与 `ok/code/error`,不会泄露 HTTP Headers;同时验证子 Action 能继承父 Action 的投影策略。 + ## ⏱ 性能基准测试 (Benchmark) 使用 `go test -bench=. ./...` 评估框架调用阶段的开销。 > **基准**: Darwin / Apple M3 Max -* `BenchmarkCallEngineLogic-16`:约 **118.9 ns/op**, **2 allocs/op**。 +* `BenchmarkCallEngineLogic-16`:约 **115.4 ns/op**, **80 B/op**, **2 allocs/op**。 该指标证明引擎的参数合并、注入及校验流程具有极高的运行效率和极小的内存逃逸。 ## 🚀 运行测试 diff --git a/js_export.go b/js_export.go index 57803f5..9b0727e 100644 --- a/js_export.go +++ b/js_export.go @@ -3,6 +3,7 @@ package api import ( "context" + "apigo.cc/go/cast" "apigo.cc/go/jsmod" ) @@ -22,6 +23,9 @@ func call(ctx context.Context, name string, payload any, options ...*CallOptions opts := firstOptions(options) opts.Context = ctx res, err := CallBy(name, payload, opts) + if actionUsesDataResult(name) { + return projectDataResult(res), nil + } if err != nil { if res != nil { return res, nil @@ -31,6 +35,33 @@ func call(ctx context.Context, name string, payload any, options ...*CallOptions return res, nil } +func actionUsesDataResult(name string) bool { + config, err := resolveGenericActionConfig(name, map[string]bool{}) + return err == nil && cast.String(config["resultMode"]) == "data" +} + +func projectDataResult(result *Result) any { + if result == nil { + return map[string]any{"ok": false, "code": "", "error": "empty API result"} + } + if data, ok := result.Data.(map[string]any); ok { + output := cloneMap(data) + if _, exists := output["ok"]; !exists { + output["ok"] = result.Ok + } + if _, exists := output["code"]; !exists { + output["code"] = result.Code + } + if _, exists := output["error"]; !exists { + output["error"] = result.Error + } + return output + } + return map[string]any{ + "ok": result.Ok, "code": result.Code, "error": result.Error, "result": result.Data, + } +} + // registerSigner 允许从 JS 注册动态签名逻辑 func registerSigner(name string, code string) { RegisterSigner(name, &jsSigner{code: code}) diff --git a/js_export_internal_test.go b/js_export_internal_test.go new file mode 100644 index 0000000..0e7ba0a --- /dev/null +++ b/js_export_internal_test.go @@ -0,0 +1,24 @@ +package api + +import "testing" + +func TestProjectDataResult(t *testing.T) { + result := &Result{Ok: true, StatusCode: 200, Headers: map[string]string{"X-Test": "ignored"}, Data: map[string]any{"result": "hello"}} + projected := projectDataResult(result).(map[string]any) + if projected["ok"] != true || projected["result"] != "hello" || projected["code"] != "" || projected["error"] != "" { + t.Fatalf("unexpected projected result: %#v", projected) + } + if _, exists := projected["headers"]; exists { + t.Fatalf("transport headers leaked into projected result: %#v", projected) + } +} + +func TestActionUsesInheritedDataResult(t *testing.T) { + parent, child := t.Name()+".parent", t.Name()+".child" + RegisterAction(parent, map[string]any{"abstract": true, "resultMode": "data"}) + RegisterAction(child, map[string]any{"extends": parent, "url": "http://127.0.0.1"}) + t.Cleanup(func() { RemoveAction(child); RemoveAction(parent) }) + if !actionUsesDataResult(child) { + t.Fatal("child Action did not inherit resultMode=data") + } +}