2022-12-09 10:09:54 -08:00

377 lines
9.1 KiB
Go

package engine
import (
"fmt"
"net/http"
"unsafe"
"github.com/benallfree/pbscript/modules/pbscript/event"
"github.com/goccy/go-json"
"github.com/dop251/goja"
"github.com/labstack/echo/v5"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
)
var app *pocketbase.PocketBase
var router *echo.Echo
var vm *goja.Runtime
var cleanups = []func(){}
var __go_apis *goja.Object
const (
colorReset = "\033[0m"
colorRed = "\033[31m"
colorGreen = "\033[32m"
colorYellow = "\033[33m"
colorBlue = "\033[34m"
colorPurple = "\033[35m"
colorCyan = "\033[36m"
colorWhite = "\033[37m"
)
func logErrorf(format string, args ...any) (n int, err error) {
s := append(args, string(colorReset))
fmt.Print(colorRed)
res, err := fmt.Printf(format, s...)
fmt.Print(colorReset)
return res, err
}
func bindApis() {
__go_apis = vm.NewObject()
__go_apis.Set("addRoute", func(route echo.Route) {
method := route.Method
path := route.Path
fmt.Printf("Adding route: %s %s\n", method, path)
router.AddRoute(route)
cleanup(
fmt.Sprintf("route %s %s", method, path),
func() {
router.Router().Remove(method, path)
})
})
__go_apis.Set("onModelBeforeCreate", func(cb func(e *core.ModelEvent)) {
fmt.Println("Listening in Go for onModelBeforeCreate")
unsub := event.On(event.EVT_ON_MODEL_BEFORE_CREATE, func(e *event.UnknownPayload) {
// fmt.Println("syntheticevent: OnModelBeforeCreate")
// fmt.Println("e", e)
// fmt.Println("cb", cb)
cb((*core.ModelEvent)(unsafe.Pointer(e)))
})
cleanup("onModelBeforeCreate", unsub)
})
__go_apis.Set("onModelAfterCreate", func(cb func(e *core.ModelEvent)) {
fmt.Println("Listening in Go for onModelAfterCreate")
unsub := event.On(event.EVT_ON_MODEL_AFTER_CREATE, func(e *event.UnknownPayload) {
// fmt.Println("syntheticevent: OnModelAfterCreate")
// fmt.Println("e", e)
// fmt.Println("cb", cb)
cb((*core.ModelEvent)(unsafe.Pointer(e)))
})
cleanup("onModelAfterCreate", unsub)
})
// type TransactionApi struct {
// Execute func(sql string)
// }
// __go_apis.Set("withTransaction", func(cb func(e *TransactionApi)) {
// app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
// var api = TransactionApi{
// Execute: func(sql string) error {
// res, err := txDao.DB().Select().NewQuery(sql).Execute()
// if err != nil {
// return err
// }
// }}
// })
// })
__go_apis.Set("requireAdminAuth", apis.RequireAdminAuth)
__go_apis.Set("requireAdminAuthOnlyIfAny", apis.RequireAdminAuthOnlyIfAny)
__go_apis.Set("requireAdminOrOwnerAuth", apis.RequireAdminOrOwnerAuth)
__go_apis.Set("requireAdminOrUserAuth", apis.RequireAdminOrUserAuth)
__go_apis.Set("app", app)
__go_apis.Set("ping", func() string {
return "Hello from Go!"
})
__go_apis.Set("newNullStringMapArrayPtr", func() *[]dbx.NullStringMap {
var users2 []dbx.NullStringMap
return &users2
})
__go_apis.Set("newNullStringMap", func() dbx.NullStringMap {
var users2 dbx.NullStringMap
return users2
})
}
func cleanup(msg string, cb func()) {
fmt.Printf("adding cleanup: %s\n", msg)
cleanups = append(cleanups, func() {
fmt.Printf("executing cleanup: %s\n", msg)
cb()
})
}
func loadActiveScript() (string, error) {
collection, err := app.Dao().FindCollectionByNameOrId("pbscript")
if err != nil {
return "", err
}
recs, err := app.Dao().FindRecordsByExpr(collection, dbx.HashExp{"type": "script", "isActive": true})
if err != nil {
return "", err
}
if len(recs) > 1 {
return "", fmt.Errorf("expected one active script record but got %d", len(recs))
}
if len(recs) == 0 {
return "", nil // Empty script
}
rec := recs[0]
jsonData := rec.GetStringDataValue("data")
type Data struct {
Source string `json:"source"`
}
var json_map Data
err = json.Unmarshal([]byte(jsonData), &json_map)
if err != nil {
return "", err
}
script := json_map.Source
fmt.Printf("Script has been loaded.\n")
return script, nil
}
func reloadVm() error {
fmt.Println("Initializing PBScript engine")
vm = goja.New()
vm.SetFieldNameMapper(goja.UncapFieldNameMapper())
// Clean up all handlers
fmt.Println("Executing cleanups")
for i := 0; i < len(cleanups); i++ {
cleanups[i]()
}
cleanups = nil
// Load the main script
fmt.Println("Loading JS")
script, err := loadActiveScript()
if err != nil {
return err
}
// Console proxy
fmt.Println("Creating console proxy")
console := vm.NewObject()
console.Set("log", func(s ...goja.Value) {
for _, v := range s {
fmt.Printf("%s ", v.String())
}
fmt.Print("\n")
})
vm.Set("console", console)
fmt.Println("Creating apis proxy")
bindApis()
vm.Set("__go", __go_apis)
fmt.Println("Go initialization complete. Running script.")
source := fmt.Sprintf(`
console.log('Top of PBScript bootstrap')
let __jsfuncs = {ping: ()=>'Hello from PBScript!'}
function registerJsFuncs(funcs) {
__jsfuncs = {__jsfuncs, ...funcs }
}
%s
console.log('Pinging Go')
console.log('Pinging Go succeeded with:', __go.ping())
console.log('Bottom of PBScript bootstrap')
`, script)
_, err = vm.RunString(source)
if err != nil {
return err
}
// js api wireup
fmt.Println("Wiring up JS API")
type S struct {
Ping func() (string, *goja.Exception) `json:"ping"`
}
jsFuncs := S{}
err = vm.ExportTo(vm.Get("__jsfuncs"), &jsFuncs)
if err != nil {
return err
}
{
fmt.Println("Pinging JS")
res, err := jsFuncs.Ping()
if err != nil {
return fmt.Errorf("ping() failed with %s", err.Value().Export())
} else {
fmt.Printf("Ping succeeded with: %s\n", res)
}
}
return nil
}
func migrate() error {
fmt.Println("Finding collection")
_, err := app.Dao().FindCollectionByNameOrId("anything")
fmt.Println("Finished collection")
if err != nil {
err = app.Dao().SaveCollection(&models.Collection{
Name: "pbscript",
Schema: schema.NewSchema(
&schema.SchemaField{
Type: schema.FieldTypeText,
Name: "type",
},
&schema.SchemaField{
Type: schema.FieldTypeBool,
Name: "isActive",
},
&schema.SchemaField{
Type: schema.FieldTypeJson,
Name: "data",
},
),
})
if err != nil {
return err
}
}
return nil
}
func watchForScriptChanges() {
app.OnModelAfterUpdate().Add(func(e *core.ModelEvent) error {
if e.Model.TableName() == "pbscript" {
reloadVm()
}
return nil
})
app.OnModelAfterCreate().Add(func(e *core.ModelEvent) error {
if e.Model.TableName() == "pbscript" {
reloadVm()
}
return nil
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// add new "GET /api/hello" route
e.Router.AddRoute(echo.Route{
Method: http.MethodPost,
Path: "/api/pbscript/deploy",
Handler: func(c echo.Context) error {
json_map := make(map[string]interface{})
err := json.NewDecoder(c.Request().Body).Decode(&json_map)
if err != nil {
return err
}
//json_map has the JSON Payload decoded into a map
src := json_map["source"]
err = app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
fmt.Println("Deactivating active script")
_, err := txDao.DB().
NewQuery("UPDATE pbscript SET isActive=false WHERE type='script'").Execute()
if err != nil {
return err
}
fmt.Println("Packaging new record data")
bytes, err := json.Marshal(dbx.Params{"source": src})
if err != nil {
return err
}
_json := string(bytes)
fmt.Println("Saving new model")
collection, err := txDao.FindCollectionByNameOrId("pbscript")
if err != nil {
return err
}
record := models.NewRecord(collection)
record.SetDataValue("type", "script")
record.SetDataValue("isActive", "true")
record.SetDataValue("data", _json)
err = txDao.SaveRecord(record)
if err != nil {
return err
}
fmt.Println(("Record saved"))
// _, err = txDao.DB().
// NewQuery("INSERT INTO pbscript (type,isActive,data) values ('script', true, {data})").Bind(dbx.Params{"data": _json}).Execute()
// if err != nil {
// return err
// }
return nil
})
if err != nil {
return err
}
return c.String(http.StatusOK, "ok")
},
Middlewares: []echo.MiddlewareFunc{
apis.RequireAdminAuth(),
},
})
return nil
})
}
func initAppEvents() {
app.OnModelBeforeCreate().Add(func(e *core.ModelEvent) error {
fmt.Println("event: OnModelBeforeCreate")
event.Fire(event.EVT_ON_MODEL_BEFORE_CREATE, (*event.UnknownPayload)(unsafe.Pointer(e)))
return nil
})
app.OnModelAfterCreate().Add(func(e *core.ModelEvent) error {
fmt.Println("event: OnModelAfterCreate")
event.Fire(event.EVT_ON_MODEL_AFTER_CREATE, (*event.UnknownPayload)(unsafe.Pointer(e)))
return nil
})
}
func StartPBScript(_app *pocketbase.PocketBase) error {
app = _app
watchForScriptChanges()
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
migrate()
initAppEvents()
router = e.Router
err := reloadVm()
if err != nil {
logErrorf("Error loading VM: %s\n", err)
}
return nil
})
return nil
}