fix(js): 统一返回对象方法桥接(by AI)
This commit is contained in:
parent
1fa63e7e3c
commit
0786841088
@ -1,5 +1,10 @@
|
|||||||
# CHANGELOG - go/js
|
# CHANGELOG - go/js
|
||||||
|
|
||||||
|
## v1.5.8 (2026-07-13)
|
||||||
|
- **桥接一致性修复**: Go 函数返回的 API 门面对象,其导出方法也统一经由桥接层调用。
|
||||||
|
JavaScript 的 lower-camel 对象字段会与顶层 API 一样自动转换为 Go struct 字段。
|
||||||
|
- **回归测试**: 覆盖 `table().Query({filter: [...]})` 形式的返回对象方法调用。
|
||||||
|
|
||||||
## v1.5.7 (2026-06-28)
|
## v1.5.7 (2026-06-28)
|
||||||
- **低代码 TS 文档对齐**:
|
- **低代码 TS 文档对齐**:
|
||||||
- `Doc()` 生成的模块入口改为顶层全局变量声明,如 `declare const api: Api_Module;`,不再使用 `declare const go: { ... }` 包裹。
|
- `Doc()` 生成的模块入口改为顶层全局变量声明,如 `declare const api: Api_Module;`,不再使用 `declare const go: { ... }` 包裹。
|
||||||
|
|||||||
17
README.md
17
README.md
@ -36,11 +36,11 @@ import "apigo.cc/go/js"
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Check if script needs update (e.g., from file mtime)
|
// Check if script needs update (e.g., from file mtime)
|
||||||
if !js.CheckVersion("myTask.js", mtime) {
|
if !js.CheckVersion("myTask", mtime) {
|
||||||
js.Define(code, "myTask.js", mtime)
|
js.Define("myTask", code, mtime)
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := js.Call(ctx, "myTask", "star")
|
res, err := js.Call("myTask", time.Second, nil, "star")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -79,9 +79,18 @@ interface Logger {
|
|||||||
|
|
||||||
## Internal Bridge Details
|
## Internal Bridge Details
|
||||||
|
|
||||||
The engine uses `goja`'s Host Object mechanism. When a Go struct/pointer is returned to JS, it remains a Go object. When passed back to a Go function, the original pointer is preserved, ensuring zero data loss and state consistency.
|
The engine preserves ordinary Go data values. API facade objects returned to JavaScript expose their exported methods through the same bridge as top-level module functions, ensuring identical context injection, safe-mode checking, error conversion, and struct conversion.
|
||||||
|
|
||||||
Types are automatically coerced:
|
Types are automatically coerced:
|
||||||
- JS `string` -> Go `int` (via `go/cast`)
|
- JS `string` -> Go `int` (via `go/cast`)
|
||||||
- JS `Object` -> Go `Struct`
|
- JS `Object` -> Go `Struct`
|
||||||
- Go `error` -> JS `Exception`
|
- Go `error` -> JS `Exception`
|
||||||
|
|
||||||
|
Therefore both styles use normal JavaScript lower-camel fields:
|
||||||
|
|
||||||
|
```js
|
||||||
|
knowbase.Table("Account").Query({
|
||||||
|
filter: [{ field: "Name", operator: "=", value: "Admin" }],
|
||||||
|
limit: 1000
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|||||||
3
TEST.md
3
TEST.md
@ -33,6 +33,8 @@ CPU: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
|
|||||||
--- PASS: TestBridgeOptionalParams (0.00s)
|
--- PASS: TestBridgeOptionalParams (0.00s)
|
||||||
=== RUN TestBridgeVariadic
|
=== RUN TestBridgeVariadic
|
||||||
--- PASS: TestBridgeVariadic (0.00s)
|
--- PASS: TestBridgeVariadic (0.00s)
|
||||||
|
=== RUN TestBridgeMapsLowerCamelCaseForReturnedObjectMethods
|
||||||
|
--- PASS: TestBridgeMapsLowerCamelCaseForReturnedObjectMethods (0.00s)
|
||||||
=== RUN TestDocGeneration
|
=== RUN TestDocGeneration
|
||||||
--- PASS: TestDocGeneration (0.00s)
|
--- PASS: TestDocGeneration (0.00s)
|
||||||
=== RUN TestPoolVersioning
|
=== RUN TestPoolVersioning
|
||||||
@ -55,6 +57,7 @@ ok apigo.cc/go/js 0.800s
|
|||||||
|
|
||||||
## Features Verified
|
## Features Verified
|
||||||
- [x] JS calling Go with automatic type conversion.
|
- [x] JS calling Go with automatic type conversion.
|
||||||
|
- [x] Returned Go API object methods use the same lower-camel struct conversion.
|
||||||
- [x] Go pointers preservation in JS.
|
- [x] Go pointers preservation in JS.
|
||||||
- [x] Context and Logger injection.
|
- [x] Context and Logger injection.
|
||||||
- [x] Concurrent execution and script versioning.
|
- [x] Concurrent execution and script versioning.
|
||||||
|
|||||||
45
bridge.go
45
bridge.go
@ -164,17 +164,58 @@ func wrapGoFunc(vm *goja.Runtime, fn any, isUnsafe bool) goja.Value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(results) == 1 {
|
if len(results) == 1 {
|
||||||
return vm.ToValue(results[0].Interface())
|
return toJSValue(vm, results[0].Interface())
|
||||||
}
|
}
|
||||||
|
|
||||||
resSlice := make([]any, len(results))
|
resSlice := make([]any, len(results))
|
||||||
for i, r := range results {
|
for i, r := range results {
|
||||||
resSlice[i] = r.Interface()
|
resSlice[i] = toJSValue(vm, r.Interface())
|
||||||
}
|
}
|
||||||
return vm.ToValue(resSlice)
|
return vm.ToValue(resSlice)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toJSValue exposes Go API objects through bridge-wrapped methods rather than
|
||||||
|
// goja's direct reflection. Directly reflected methods bypass cast.Convert, so
|
||||||
|
// nested lower-camel JavaScript objects would not be converted to Go structs.
|
||||||
|
// Plain data structs retain goja's normal value export behavior.
|
||||||
|
func toJSValue(vm *goja.Runtime, value any) goja.Value {
|
||||||
|
reflectValue := reflect.ValueOf(value)
|
||||||
|
if !reflectValue.IsValid() {
|
||||||
|
return goja.Null()
|
||||||
|
}
|
||||||
|
reflectType := reflectValue.Type()
|
||||||
|
structType := reflectType
|
||||||
|
if structType.Kind() == reflect.Pointer {
|
||||||
|
if reflectValue.IsNil() {
|
||||||
|
return goja.Null()
|
||||||
|
}
|
||||||
|
structType = structType.Elem()
|
||||||
|
}
|
||||||
|
if structType.Kind() != reflect.Struct || reflectType.NumMethod() == 0 || hasExportedField(structType) {
|
||||||
|
return vm.ToValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
object := vm.NewObject()
|
||||||
|
for index := 0; index < reflectType.NumMethod(); index++ {
|
||||||
|
method := reflectType.Method(index)
|
||||||
|
boundMethod := reflectValue.Method(index)
|
||||||
|
if boundMethod.IsValid() {
|
||||||
|
_ = object.Set(method.Name, wrapGoFunc(vm, boundMethod.Interface(), false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasExportedField(structType reflect.Type) bool {
|
||||||
|
for index := 0; index < structType.NumField(); index++ {
|
||||||
|
if structType.Field(index).IsExported() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// trimGoPath shortens a Go source file path for display:
|
// trimGoPath shortens a Go source file path for display:
|
||||||
// keeps only the last two path components (package/file.go).
|
// keeps only the last two path components (package/file.go).
|
||||||
func trimGoPath(fullPath string) string {
|
func trimGoPath(fullPath string) string {
|
||||||
|
|||||||
@ -13,6 +13,27 @@ import (
|
|||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type bridgeFacadeFilter struct {
|
||||||
|
Field string
|
||||||
|
Operator string
|
||||||
|
Value any
|
||||||
|
}
|
||||||
|
|
||||||
|
type bridgeFacadeRequest struct {
|
||||||
|
Filter []bridgeFacadeFilter
|
||||||
|
Limit int
|
||||||
|
}
|
||||||
|
|
||||||
|
// bridgeFacade intentionally follows the shape used by Go APIs exposed to JS:
|
||||||
|
// private state and exported methods.
|
||||||
|
type bridgeFacade struct {
|
||||||
|
received bridgeFacadeRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *bridgeFacade) Query(request bridgeFacadeRequest) {
|
||||||
|
f.received = request
|
||||||
|
}
|
||||||
|
|
||||||
func TestBridgeSafeMode(t *testing.T) {
|
func TestBridgeSafeMode(t *testing.T) {
|
||||||
vm := goja.New()
|
vm := goja.New()
|
||||||
|
|
||||||
@ -145,3 +166,23 @@ func TestBridgeVariadic(t *testing.T) {
|
|||||||
t.Errorf("Variadic args content wrong, got %v", result)
|
t.Errorf("Variadic args content wrong, got %v", result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBridgeMapsLowerCamelCaseForReturnedObjectMethods(t *testing.T) {
|
||||||
|
vm := goja.New()
|
||||||
|
facade := &bridgeFacade{}
|
||||||
|
if err := vm.Set("table", wrapGoFunc(vm, func() *bridgeFacade { return facade }, false)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := vm.RunString(`table().Query({filter: [{field: "Name", operator: "=", value: "Admin"}], limit: 1000})`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("JS execution failed: %v", err)
|
||||||
|
}
|
||||||
|
if facade.received.Limit != 1000 || len(facade.received.Filter) != 1 {
|
||||||
|
t.Fatalf("lower-camel request was not converted: %#v", facade.received)
|
||||||
|
}
|
||||||
|
filter := facade.received.Filter[0]
|
||||||
|
if filter.Field != "Name" || filter.Operator != "=" || filter.Value != "Admin" {
|
||||||
|
t.Errorf("unexpected converted filter: %#v", filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user