package plugins import ( "fmt" "verstak/internal/core/nodes" lua "github.com/yuin/gopher-lua" ) // nodeToTable converts a Node to a Lua table. func nodeToTable(L *lua.LState, n nodeOrErr) *lua.LTable { tbl := L.NewTable() if n.err != nil { return tbl } tbl.RawSetString("id", lua.LString(n.node.ID)) tbl.RawSetString("title", lua.LString(n.node.Title)) tbl.RawSetString("type", lua.LString(n.node.Type)) tbl.RawSetString("slug", lua.LString(n.node.Slug)) tbl.RawSetString("sort_order", lua.LNumber(n.node.SortOrder)) tbl.RawSetString("archived", lua.LBool(n.node.Archived)) tbl.RawSetString("revision", lua.LNumber(n.node.Revision)) tbl.RawSetString("created_at", lua.LString(n.node.CreatedAt.Format(timeFormat))) tbl.RawSetString("updated_at", lua.LString(n.node.UpdatedAt.Format(timeFormat))) if n.node.ParentID != nil { tbl.RawSetString("parent_id", lua.LString(*n.node.ParentID)) } if n.node.DeletedAt != nil { tbl.RawSetString("deleted_at", lua.LString(n.node.DeletedAt.Format(timeFormat))) } return tbl } // registerNodeAPI registers the verstak.node.* API and returns the table. func registerNodeAPI(vm *LuaVM) *lua.LTable { svc := vm.Services if svc == nil || svc.NodeRepo == nil { // No services available — return empty table but still register it emptyTbl := vm.L.NewTable() vm.L.SetGlobal("verstak_node", emptyTbl) return emptyTbl } L := vm.L tbl := L.NewTable() // verstak.node.get(id) → table or nil tbl.RawSetString("get", L.NewFunction(func(L *lua.LState) int { id := L.CheckString(1) n, err := svc.NodeRepo.GetActive(id) if err != nil { return pushError(L, err) } tbl := nodeToTable(L, nodeOrErr{node: n}) L.Push(tbl) return 1 })) // verstak.node.list(parent_id) → array of tables tbl.RawSetString("list", L.NewFunction(func(L *lua.LState) int { parentID := L.CheckString(1) children, err := svc.NodeRepo.ListChildren(parentID, false) if err != nil { return pushError(L, err) } arr := L.NewTable() for i, n := range children { tbl := nodeToTable(L, nodeOrErr{node: &n}) arr.RawSetInt(i+1, tbl) } L.Push(arr) return 1 })) // verstak.node.create(parent_id, title, type) → table tbl.RawSetString("create", L.NewFunction(func(L *lua.LState) int { parentID := lua.LNil if L.GetTop() >= 1 && L.Get(1) != lua.LNil { parentID = L.Get(1) } title := L.CheckString(2) typ := L.OptString(3, "document") _ = L.OptString(4, "") // props (reserved) var pID *string if parentID != lua.LNil { s := lua.LVAsString(parentID) if s != "" { pID = &s } } n, err := svc.NodeRepo.Create(pID, typ, title, 0, "", "") if err != nil { return pushError(L, err) } tbl := nodeToTable(L, nodeOrErr{node: n}) L.Push(tbl) return 1 })) // verstak.node.update(id, fields) → success/error tbl.RawSetString("update", L.NewFunction(func(L *lua.LState) int { id := L.CheckString(1) fields := L.CheckTable(2) titleVal := fields.RawGetString("title") if titleVal != lua.LNil { if err := svc.NodeRepo.UpdateTitle(id, lua.LVAsString(titleVal)); err != nil { return pushError(L, err) } } L.Push(lua.LBool(true)) return 1 })) // verstak.node.delete(id) → success/error tbl.RawSetString("delete", L.NewFunction(func(L *lua.LState) int { id := L.CheckString(1) if err := svc.NodeRepo.SoftDelete(id); err != nil { return pushError(L, err) } L.Push(lua.LBool(true)) return 1 })) // verstak.node.search(query) → array of tables tbl.RawSetString("search", L.NewFunction(func(L *lua.LState) int { query := L.CheckString(1) limit := L.OptInt(2, 20) results, err := svc.NodeRepo.Search(query, limit) if err != nil { return pushError(L, err) } arr := L.NewTable() for i, n := range results { tbl := nodeToTable(L, nodeOrErr{node: &n}) arr.RawSetInt(i+1, tbl) } L.Push(arr) return 1 })) // verstak.node.roots() → array of root-level nodes tbl.RawSetString("roots", L.NewFunction(func(L *lua.LState) int { roots, err := svc.NodeRepo.ListRoots(false) if err != nil { return pushError(L, err) } arr := L.NewTable() for i, n := range roots { tbl := nodeToTable(L, nodeOrErr{node: &n}) arr.RawSetInt(i+1, tbl) } L.Push(arr) return 1 })) // verstak.node.meta — sub-table metaTbl := L.NewTable() metaTbl.RawSetString("get", L.NewFunction(func(L *lua.LState) int { nodeID := L.CheckString(1) key := L.CheckString(2) v, ok, err := svc.NodeRepo.MetaGet(nodeID, key) if err != nil { return pushError(L, err) } if !ok { L.Push(lua.LNil) } else { L.Push(lua.LString(v)) } return 1 })) metaTbl.RawSetString("set", L.NewFunction(func(L *lua.LState) int { nodeID := L.CheckString(1) key := L.CheckString(2) value := L.CheckString(3) if err := svc.NodeRepo.MetaSet(nodeID, key, value); err != nil { return pushError(L, err) } L.Push(lua.LBool(true)) return 1 })) metaTbl.RawSetString("list", L.NewFunction(func(L *lua.LState) int { nodeID := L.CheckString(1) metas, err := svc.NodeRepo.MetaList(nodeID) if err != nil { return pushError(L, err) } arr := L.NewTable() for i, m := range metas { entry := L.NewTable() entry.RawSetString("key", lua.LString(m.Key)) entry.RawSetString("value", lua.LString(m.Value)) arr.RawSetInt(i+1, entry) } L.Push(arr) return 1 })) tbl.RawSetString("meta", metaTbl) // Set the global L.SetGlobal("verstak_node", tbl) // Also add to the main verstak table if it exists mainTbl := L.GetGlobal("verstak") if mainTbl != lua.LNil { if tbl2, ok := mainTbl.(*lua.LTable); ok { tbl2.RawSetString("node", tbl) } } return tbl } // nodeOrErr is a helper to keep node + potential error together. type nodeOrErr struct { node *nodes.Node err error } // timeFormat for Lua output. const timeFormat = "2006-01-02T15:04:05Z07:00" // This function is used in the text below — comment to satisfy unused import. var _ = fmt.Sprintf