yao/workspace/jsapi/jsapi.go
Max df420978f0 feat(workspace): implement JSAPI with archive support and full test coverage
- tai/volume: add WithRemotePath sync option, 8 archive/compression methods
  (Zip/Unzip/Gzip/Gunzip/Tar/Untar/Tgz/Untgz) with local+remote impls
- workspace/jsapi: implement 24-method WorkspaceFS (file I/O, Copy with
  local:///tmp:// URI dispatch, archive ops) and 4 static methods
- workspace/manager: add Rename, MkdirAll, Volume() accessors via singleton
- Tests: comprehensive Go tests for tai/volume, workspace, workspace/jsapi
  covering both local and remote modes via SANDBOX_TEST_REMOTE_ADDR
- CI: update tai Docker image to latest in pr-test.yml
- sandbox/v2: add JSAPI docs and host/node module stubs

Made-with: Cursor
2026-03-08 14:17:39 +08:00

174 lines
4.5 KiB
Go

// Package jsapi registers the workspace namespace into the Yao V8 runtime.
package jsapi
import (
"context"
"encoding/json"
v8 "github.com/yaoapp/gou/runtime/v8"
"github.com/yaoapp/yao/workspace"
"rogchap.com/v8go"
)
func init() {
v8.RegisterObject("workspace", ExportObject)
}
func ExportObject(iso *v8go.Isolate) *v8go.ObjectTemplate {
obj := v8go.NewObjectTemplate(iso)
obj.Set("Create", v8go.NewFunctionTemplate(iso, wsCreate))
obj.Set("Get", v8go.NewFunctionTemplate(iso, wsGet))
obj.Set("List", v8go.NewFunctionTemplate(iso, wsList))
obj.Set("Delete", v8go.NewFunctionTemplate(iso, wsDelete))
return obj
}
func wsCreate(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := context.Background()
args := info.Args()
if len(args) < 1 || !args[0].IsObject() {
return throwError(info, "workspace.Create requires an options object")
}
optsObj, err := args[0].AsObject()
if err != nil {
return throwError(info, "invalid options: "+err.Error())
}
opts := workspace.CreateOptions{}
if v, e := optsObj.Get("id"); e == nil && v.IsString() {
opts.ID = v.String()
}
if v, e := optsObj.Get("name"); e == nil && v.IsString() {
opts.Name = v.String()
}
if v, e := optsObj.Get("owner"); e == nil && v.IsString() {
opts.Owner = v.String()
}
if v, e := optsObj.Get("node"); e == nil && v.IsString() {
opts.Node = v.String()
}
if v, e := optsObj.Get("labels"); e == nil && v.IsObject() {
opts.Labels = parseStringMapFromValue(info.Context(), v)
}
if opts.Name == "" || opts.Owner == "" || opts.Node == "" {
return throwError(info, "workspace.Create: name, owner, and node are required")
}
ws, err := workspace.M().Create(ctx, opts)
if err != nil {
return throwError(info, err.Error())
}
val, err := NewFSObject(info.Context(), ws.ID)
if err != nil {
return throwError(info, err.Error())
}
return val
}
func wsGet(info *v8go.FunctionCallbackInfo) *v8go.Value {
iso := info.Context().Isolate()
ctx := context.Background()
args := info.Args()
if len(args) < 1 || !args[0].IsString() {
return throwError(info, "workspace.Get requires a string ID")
}
id := args[0].String()
_, err := workspace.M().Get(ctx, id)
if err != nil {
return v8go.Null(iso)
}
val, err := NewFSObject(info.Context(), id)
if err != nil {
return v8go.Null(iso)
}
return val
}
func wsList(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := context.Background()
v8ctx := info.Context()
opts := workspace.ListOptions{}
args := info.Args()
if len(args) > 0 && args[0].IsObject() {
filterObj, _ := args[0].AsObject()
if filterObj != nil {
if v, e := filterObj.Get("owner"); e == nil && v.IsString() {
opts.Owner = v.String()
}
if v, e := filterObj.Get("node"); e == nil && v.IsString() {
opts.Node = v.String()
}
}
}
list, err := workspace.M().List(ctx, opts)
if err != nil {
return throwError(info, err.Error())
}
type wsInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Owner string `json:"owner"`
Node string `json:"node"`
Labels map[string]string `json:"labels,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
items := make([]wsInfo, len(list))
for i, ws := range list {
items[i] = wsInfo{
ID: ws.ID, Name: ws.Name, Owner: ws.Owner, Node: ws.Node,
Labels: ws.Labels, CreatedAt: ws.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: ws.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
}
data, _ := json.Marshal(items)
val, err := v8go.JSONParse(v8ctx, string(data))
if err != nil {
return throwError(info, err.Error())
}
return val
}
func wsDelete(info *v8go.FunctionCallbackInfo) *v8go.Value {
iso := info.Context().Isolate()
ctx := context.Background()
args := info.Args()
if len(args) < 1 || !args[0].IsString() {
return throwError(info, "workspace.Delete requires a string ID")
}
if err := workspace.M().Delete(ctx, args[0].String(), false); err != nil {
return throwError(info, err.Error())
}
return v8go.Undefined(iso)
}
func throwError(info *v8go.FunctionCallbackInfo, msg string) *v8go.Value {
iso := info.Context().Isolate()
e, _ := v8go.NewValue(iso, msg)
iso.ThrowException(e)
return v8go.Undefined(iso)
}
func parseStringMapFromValue(v8ctx *v8go.Context, val *v8go.Value) map[string]string {
result := make(map[string]string)
jsonStr, err := v8go.JSONStringify(v8ctx, val)
if err != nil {
return result
}
_ = json.Unmarshal([]byte(jsonStr), &result)
return result
}