verstak/internal/core/plugins/api_node.go

227 lines
5.8 KiB
Go

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