(no tests) updated jsvm bindings
This commit is contained in:
parent
5e37c90dde
commit
13d96e793b
|
@ -2,26 +2,212 @@ package jsvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||||
"github.com/labstack/echo/v5"
|
"github.com/labstack/echo/v5"
|
||||||
"github.com/pocketbase/dbx"
|
"github.com/pocketbase/dbx"
|
||||||
"github.com/pocketbase/pocketbase/apis"
|
"github.com/pocketbase/pocketbase/apis"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
"github.com/pocketbase/pocketbase/daos"
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
"github.com/pocketbase/pocketbase/forms"
|
"github.com/pocketbase/pocketbase/forms"
|
||||||
"github.com/pocketbase/pocketbase/models"
|
"github.com/pocketbase/pocketbase/models"
|
||||||
"github.com/pocketbase/pocketbase/models/schema"
|
"github.com/pocketbase/pocketbase/models/schema"
|
||||||
"github.com/pocketbase/pocketbase/tokens"
|
"github.com/pocketbase/pocketbase/tokens"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/cron"
|
||||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/hook"
|
||||||
"github.com/pocketbase/pocketbase/tools/inflector"
|
"github.com/pocketbase/pocketbase/tools/inflector"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/list"
|
||||||
"github.com/pocketbase/pocketbase/tools/mailer"
|
"github.com/pocketbase/pocketbase/tools/mailer"
|
||||||
"github.com/pocketbase/pocketbase/tools/security"
|
"github.com/pocketbase/pocketbase/tools/security"
|
||||||
"github.com/pocketbase/pocketbase/tools/types"
|
"github.com/pocketbase/pocketbase/tools/types"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// hooksBinds adds wrapped "on*" hook methods by reflecting on core.App.
|
||||||
|
func hooksBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
|
||||||
|
fm := FieldMapper{}
|
||||||
|
|
||||||
|
appType := reflect.TypeOf(app)
|
||||||
|
appValue := reflect.ValueOf(app)
|
||||||
|
totalMethods := appType.NumMethod()
|
||||||
|
excludeHooks := []string{"OnBeforeServe"}
|
||||||
|
|
||||||
|
for i := 0; i < totalMethods; i++ {
|
||||||
|
method := appType.Method(i)
|
||||||
|
if !strings.HasPrefix(method.Name, "On") || list.ExistInSlice(method.Name, excludeHooks) {
|
||||||
|
continue // not a hook or excluded
|
||||||
|
}
|
||||||
|
|
||||||
|
jsName := fm.MethodName(appType, method)
|
||||||
|
|
||||||
|
// register the hook to the loader
|
||||||
|
loader.Set(jsName, func(callback string, tags ...string) {
|
||||||
|
pr := goja.MustCompile("", "{("+callback+").apply(undefined, __args)}", true)
|
||||||
|
|
||||||
|
tagsAsValues := make([]reflect.Value, len(tags))
|
||||||
|
for i, tag := range tags {
|
||||||
|
tagsAsValues[i] = reflect.ValueOf(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
hookInstance := appValue.MethodByName(method.Name).Call(tagsAsValues)[0]
|
||||||
|
addFunc := hookInstance.MethodByName("Add")
|
||||||
|
|
||||||
|
handlerType := addFunc.Type().In(0)
|
||||||
|
|
||||||
|
handler := reflect.MakeFunc(handlerType, func(args []reflect.Value) (results []reflect.Value) {
|
||||||
|
handlerArgs := make([]any, len(args))
|
||||||
|
for i, arg := range args {
|
||||||
|
handlerArgs[i] = arg.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := executors.run(func(executor *goja.Runtime) error {
|
||||||
|
executor.Set("__args", handlerArgs)
|
||||||
|
res, err := executor.RunProgram(pr)
|
||||||
|
executor.Set("__args", goja.Undefined())
|
||||||
|
|
||||||
|
// check for returned hook.StopPropagation
|
||||||
|
if res != nil {
|
||||||
|
if v, ok := res.Export().(error); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for throwed hook.StopPropagation
|
||||||
|
if err != nil {
|
||||||
|
exception, ok := err.(*goja.Exception)
|
||||||
|
if ok && errors.Is(exception.Value().Export().(error), hook.StopPropagation) {
|
||||||
|
return hook.StopPropagation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
return []reflect.Value{reflect.ValueOf(&err).Elem()}
|
||||||
|
})
|
||||||
|
|
||||||
|
// register the wrapped hook handler
|
||||||
|
addFunc.Call([]reflect.Value{handler})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cronBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
|
||||||
|
jobs := cron.New()
|
||||||
|
|
||||||
|
loader.Set("cronAdd", func(jobId, cronExpr, handler string) {
|
||||||
|
pr := goja.MustCompile("", "{("+handler+").apply(undefined)}", true)
|
||||||
|
|
||||||
|
err := jobs.Add(jobId, cronExpr, func() {
|
||||||
|
executors.run(func(executor *goja.Runtime) error {
|
||||||
|
_, err := executor.RunProgram(pr)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic("[cronAdd] failed to register cron job " + jobId + ": " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// start the ticker (if not already)
|
||||||
|
if jobs.Total() > 0 && !jobs.HasStarted() {
|
||||||
|
jobs.Start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
loader.Set("cronRemove", func(jobId string) {
|
||||||
|
jobs.Remove(jobId)
|
||||||
|
|
||||||
|
// stop the ticker if there are no other jobs
|
||||||
|
if jobs.Total() == 0 {
|
||||||
|
jobs.Stop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func routerBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
|
||||||
|
loader.Set("routerAdd", func(method string, path string, handler string, middlewares ...goja.Value) {
|
||||||
|
wrappedMiddlewares, err := wrapMiddlewares(executors, middlewares...)
|
||||||
|
if err != nil {
|
||||||
|
panic("[routerAdd] failed to wrap middlewares: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
pr := goja.MustCompile("", "{("+handler+").apply(undefined, __args)}", true)
|
||||||
|
|
||||||
|
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||||
|
e.Router.Add(strings.ToUpper(method), path, func(c echo.Context) error {
|
||||||
|
return executors.run(func(executor *goja.Runtime) error {
|
||||||
|
executor.Set("__args", []any{c})
|
||||||
|
_, err := executor.RunProgram(pr)
|
||||||
|
executor.Set("__args", goja.Undefined())
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}, wrappedMiddlewares...)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
loader.Set("routerUse", func(middlewares ...goja.Value) {
|
||||||
|
wrappedMiddlewares, err := wrapMiddlewares(executors, middlewares...)
|
||||||
|
if err != nil {
|
||||||
|
panic("[routerUse] failed to wrap middlewares: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||||
|
e.Router.Use(wrappedMiddlewares...)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
loader.Set("routerPre", func(middlewares ...goja.Value) {
|
||||||
|
wrappedMiddlewares, err := wrapMiddlewares(executors, middlewares...)
|
||||||
|
if err != nil {
|
||||||
|
panic("[routerPre] failed to wrap middlewares: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||||
|
e.Router.Pre(wrappedMiddlewares...)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapMiddlewares(executors *vmsPool, rawMiddlewares ...goja.Value) ([]echo.MiddlewareFunc, error) {
|
||||||
|
wrappedMiddlewares := make([]echo.MiddlewareFunc, len(rawMiddlewares))
|
||||||
|
|
||||||
|
for i, m := range rawMiddlewares {
|
||||||
|
switch v := m.Export().(type) {
|
||||||
|
case echo.MiddlewareFunc:
|
||||||
|
// "native" middleware - no need to wrap
|
||||||
|
wrappedMiddlewares[i] = v
|
||||||
|
case func(goja.FunctionCall) goja.Value, string:
|
||||||
|
pr := goja.MustCompile("", "{(("+m.String()+").apply(undefined, __args)).apply(undefined, __args2)}", true)
|
||||||
|
|
||||||
|
wrappedMiddlewares[i] = func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
return executors.run(func(executor *goja.Runtime) error {
|
||||||
|
executor.Set("__args", []any{next})
|
||||||
|
executor.Set("__args2", []any{c})
|
||||||
|
_, err := executor.RunProgram(pr)
|
||||||
|
executor.Set("__args", goja.Undefined())
|
||||||
|
executor.Set("__args2", goja.Undefined())
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unsupported goja middleware type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrappedMiddlewares, nil
|
||||||
|
}
|
||||||
|
|
||||||
func baseBinds(vm *goja.Runtime) {
|
func baseBinds(vm *goja.Runtime) {
|
||||||
vm.SetFieldNameMapper(FieldMapper{})
|
vm.SetFieldNameMapper(FieldMapper{})
|
||||||
|
|
||||||
|
@ -48,7 +234,7 @@ func baseBinds(vm *goja.Runtime) {
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
vm.Set("$arrayOf", func(model any) any {
|
vm.Set("arrayOf", func(model any) any {
|
||||||
mt := reflect.TypeOf(model)
|
mt := reflect.TypeOf(model)
|
||||||
st := reflect.SliceOf(mt)
|
st := reflect.SliceOf(mt)
|
||||||
elem := reflect.New(st).Elem()
|
elem := reflect.New(st).Elem()
|
||||||
|
@ -147,6 +333,8 @@ func baseBinds(vm *goja.Runtime) {
|
||||||
|
|
||||||
return instanceValue
|
return instanceValue
|
||||||
})
|
})
|
||||||
|
|
||||||
|
vm.Set("$stopPropagation", hook.StopPropagation)
|
||||||
}
|
}
|
||||||
|
|
||||||
func dbxBinds(vm *goja.Runtime) {
|
func dbxBinds(vm *goja.Runtime) {
|
||||||
|
@ -253,11 +441,6 @@ func apisBinds(vm *goja.Runtime) {
|
||||||
obj := vm.NewObject()
|
obj := vm.NewObject()
|
||||||
vm.Set("$apis", obj)
|
vm.Set("$apis", obj)
|
||||||
|
|
||||||
vm.Set("Route", func(call goja.ConstructorCall) *goja.Object {
|
|
||||||
instance := &echo.Route{}
|
|
||||||
return structConstructor(vm, call, instance)
|
|
||||||
})
|
|
||||||
|
|
||||||
// middlewares
|
// middlewares
|
||||||
obj.Set("requireRecordAuth", apis.RequireRecordAuth)
|
obj.Set("requireRecordAuth", apis.RequireRecordAuth)
|
||||||
obj.Set("requireAdminAuth", apis.RequireAdminAuth)
|
obj.Set("requireAdminAuth", apis.RequireAdminAuth)
|
||||||
|
|
|
@ -38,7 +38,7 @@ func TestBaseBindsCount(t *testing.T) {
|
||||||
vm := goja.New()
|
vm := goja.New()
|
||||||
baseBinds(vm)
|
baseBinds(vm)
|
||||||
|
|
||||||
testBindsCount(vm, "this", 14, t)
|
testBindsCount(vm, "this", 15, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBaseBindsRecord(t *testing.T) {
|
func TestBaseBindsRecord(t *testing.T) {
|
||||||
|
@ -682,48 +682,10 @@ func TestApisBindsCount(t *testing.T) {
|
||||||
vm := goja.New()
|
vm := goja.New()
|
||||||
apisBinds(vm)
|
apisBinds(vm)
|
||||||
|
|
||||||
testBindsCount(vm, "this", 7, t)
|
testBindsCount(vm, "this", 6, t)
|
||||||
testBindsCount(vm, "$apis", 10, t)
|
testBindsCount(vm, "$apis", 10, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApisBindsRoute(t *testing.T) {
|
|
||||||
app, _ := tests.NewTestApp()
|
|
||||||
defer app.Cleanup()
|
|
||||||
|
|
||||||
vm := goja.New()
|
|
||||||
baseBinds(vm)
|
|
||||||
apisBinds(vm)
|
|
||||||
|
|
||||||
_, err := vm.RunString(`
|
|
||||||
let handlerCalls = 0;
|
|
||||||
|
|
||||||
const route = new Route({
|
|
||||||
method: "test_method",
|
|
||||||
path: "test_path",
|
|
||||||
handler: () => {
|
|
||||||
handlerCalls++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
route.handler();
|
|
||||||
|
|
||||||
if (handlerCalls != 1) {
|
|
||||||
throw new Error('Expected handlerCalls 1, got ' + handlerCalls);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route.method != "test_method") {
|
|
||||||
throw new Error('Expected method "test_method", got ' + route.method);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route.path != "test_path") {
|
|
||||||
throw new Error('Expected method "test_path", got ' + route.path);
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestApisBindsApiError(t *testing.T) {
|
func TestApisBindsApiError(t *testing.T) {
|
||||||
app, _ := tests.NewTestApp()
|
app, _ := tests.NewTestApp()
|
||||||
defer app.Cleanup()
|
defer app.Cleanup()
|
||||||
|
@ -843,7 +805,7 @@ func TestLoadingArrayOf(t *testing.T) {
|
||||||
vm.Set("$app", app)
|
vm.Set("$app", app)
|
||||||
|
|
||||||
_, err := vm.RunString(`
|
_, err := vm.RunString(`
|
||||||
let result = $arrayOf(new DynamicModel({
|
let result = arrayOf(new DynamicModel({
|
||||||
id: "",
|
id: "",
|
||||||
text: "",
|
text: "",
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -4,16 +4,96 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
"github.com/pocketbase/pocketbase/plugins/jsvm"
|
"github.com/pocketbase/pocketbase/plugins/jsvm"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/list"
|
||||||
"github.com/pocketbase/tygoja"
|
"github.com/pocketbase/tygoja"
|
||||||
)
|
)
|
||||||
|
|
||||||
const heading = `
|
const heading = `
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// routerBinds
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RouterAdd registers a new route definition.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* ` + "```" + `js
|
||||||
|
* routerAdd("GET", "/hello", (c) => {
|
||||||
|
* return c.json(200, {"message": "Hello!"})
|
||||||
|
* }, $apis.requireAdminOrRecordAuth())
|
||||||
|
* ` + "```" + `
|
||||||
|
*
|
||||||
|
* _Note that this method is available only in pb_hooks context._
|
||||||
|
*
|
||||||
|
* @group PocketBase
|
||||||
|
*/
|
||||||
|
declare function routerAdd(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
handler: echo.HandlerFunc,
|
||||||
|
...middlewares: Array<string|echo.MiddlewareFunc>,
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RouterUse registers one or more global middlewares that are executed
|
||||||
|
* along the handler middlewares after a matching route is found.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* ` + "```" + `js
|
||||||
|
* routerUse((next) => {
|
||||||
|
* return (c) => {
|
||||||
|
* console.log(c.Path())
|
||||||
|
* return next(c)
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
* ` + "```" + `
|
||||||
|
*
|
||||||
|
* _Note that this method is available only in pb_hooks context._
|
||||||
|
*
|
||||||
|
* @group PocketBase
|
||||||
|
*/
|
||||||
|
declare function routerUse(...middlewares: Array<string|echo.MiddlewareFunc>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RouterPre registers one or more global middlewares that are executed
|
||||||
|
* BEFORE the router processes the request. It is usually used for making
|
||||||
|
* changes to the request properties, for example, adding or removing
|
||||||
|
* a trailing slash or adding segments to a path so it matches a route.
|
||||||
|
*
|
||||||
|
* NB! Since the router will not have processed the request yet,
|
||||||
|
* middlewares registered at this level won't have access to any path
|
||||||
|
* related APIs from echo.Context.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* ` + "```" + `js
|
||||||
|
* routerPre((next) => {
|
||||||
|
* return (c) => {
|
||||||
|
* console.log(c.request().url)
|
||||||
|
* return next(c)
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
* ` + "```" + `
|
||||||
|
*
|
||||||
|
* _Note that this method is available only in pb_hooks context._
|
||||||
|
*
|
||||||
|
* @group PocketBase
|
||||||
|
*/
|
||||||
|
declare function routerPre(...middlewares: Array<string|echo.MiddlewareFunc>): void;
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// baseBinds
|
// baseBinds
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
// skip on* hook methods as they are registered via the global on* method
|
||||||
|
type appWithoutHooks = Omit<pocketbase.PocketBase, ` + "`on${string}`" + `>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* $app is the current running PocketBase instance that is globally
|
* $app is the current running PocketBase instance that is globally
|
||||||
* available in each .pb.js file.
|
* available in each .pb.js file.
|
||||||
|
@ -21,23 +101,23 @@ const heading = `
|
||||||
* @namespace
|
* @namespace
|
||||||
* @group PocketBase
|
* @group PocketBase
|
||||||
*/
|
*/
|
||||||
declare var $app: pocketbase.PocketBase
|
declare var $app: appWithoutHooks
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* $arrayOf creates a placeholder array of the specified models.
|
* arrayOf creates a placeholder array of the specified models.
|
||||||
* Usually used to populate DB result into an array of models.
|
* Usually used to populate DB result into an array of models.
|
||||||
*
|
*
|
||||||
* Example:
|
* Example:
|
||||||
*
|
*
|
||||||
* ` + "```" + `js
|
* ` + "```" + `js
|
||||||
* const records = $arrayOf(new Record)
|
* const records = arrayOf(new Record)
|
||||||
*
|
*
|
||||||
* $app.dao().recordQuery(collection).limit(10).all(records)
|
* $app.dao().recordQuery(collection).limit(10).all(records)
|
||||||
* ` + "```" + `
|
* ` + "```" + `
|
||||||
*
|
*
|
||||||
* @group PocketBase
|
* @group PocketBase
|
||||||
*/
|
*/
|
||||||
declare function $arrayOf<T>(model: T): Array<T>;
|
declare function arrayOf<T>(model: T): Array<T>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DynamicModel creates a new dynamic model with fields from the provided data shape.
|
* DynamicModel creates a new dynamic model with fields from the provided data shape.
|
||||||
|
@ -498,27 +578,6 @@ declare class TestS3FilesystemForm implements forms.TestS3Filesystem {
|
||||||
// apisBinds
|
// apisBinds
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
interface Route extends echo.Route{} // merge
|
|
||||||
/**
|
|
||||||
* Route specifies a new route definition.
|
|
||||||
* This is usually used when registering routes with router.addRoute().
|
|
||||||
*
|
|
||||||
* ` + "```" + `js
|
|
||||||
* const route = new Route({
|
|
||||||
* path: "/hello",
|
|
||||||
* handler: (c) => {
|
|
||||||
* c.string(200, "hello world!")
|
|
||||||
* },
|
|
||||||
* middlewares: [$apis.activityLogger($app)]
|
|
||||||
* })
|
|
||||||
* ` + "```" + `
|
|
||||||
*
|
|
||||||
* @group PocketBase
|
|
||||||
*/
|
|
||||||
declare class Route implements echo.Route {
|
|
||||||
constructor(data?: Partial<echo.Route>)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApiError extends apis.ApiError{} // merge
|
interface ApiError extends apis.ApiError{} // merge
|
||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
|
@ -594,7 +653,7 @@ declare namespace $apis {
|
||||||
/**
|
/**
|
||||||
* Migrate defines a single migration upgrade/downgrade action.
|
* Migrate defines a single migration upgrade/downgrade action.
|
||||||
*
|
*
|
||||||
* Note that this method is available only in pb_migrations context.
|
* _Note that this method is available only in pb_migrations context._
|
||||||
*
|
*
|
||||||
* @group PocketBase
|
* @group PocketBase
|
||||||
*/
|
*/
|
||||||
|
@ -604,8 +663,10 @@ declare function migrate(
|
||||||
): void;
|
): void;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
var mapper = &jsvm.FieldMapper{}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
mapper := &jsvm.FieldMapper{}
|
declarations := heading + hooksDeclarations()
|
||||||
|
|
||||||
gen := tygoja.New(tygoja.Config{
|
gen := tygoja.New(tygoja.Config{
|
||||||
Packages: map[string][]string{
|
Packages: map[string][]string{
|
||||||
|
@ -626,7 +687,7 @@ func main() {
|
||||||
},
|
},
|
||||||
Indent: " ", // use only a single space to reduce slight the size
|
Indent: " ", // use only a single space to reduce slight the size
|
||||||
WithPackageFunctions: true,
|
WithPackageFunctions: true,
|
||||||
Heading: heading,
|
Heading: declarations,
|
||||||
})
|
})
|
||||||
|
|
||||||
result, err := gen.Generate()
|
result, err := gen.Generate()
|
||||||
|
@ -638,3 +699,45 @@ func main() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hooksDeclarations() string {
|
||||||
|
var result strings.Builder
|
||||||
|
|
||||||
|
excluded := []string{"OnBeforeServe"}
|
||||||
|
appType := reflect.TypeOf(struct{ core.App }{})
|
||||||
|
totalMethods := appType.NumMethod()
|
||||||
|
|
||||||
|
for i := 0; i < totalMethods; i++ {
|
||||||
|
method := appType.Method(i)
|
||||||
|
if !strings.HasPrefix(method.Name, "On") || list.ExistInSlice(method.Name, excluded) {
|
||||||
|
continue // not a hook or excluded
|
||||||
|
}
|
||||||
|
|
||||||
|
hookType := method.Type.Out(0)
|
||||||
|
|
||||||
|
withTags := strings.HasPrefix(hookType.String(), "*hook.TaggedHook")
|
||||||
|
|
||||||
|
addMethod, ok := hookType.MethodByName("Add")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
addHanlder := addMethod.Type.In(1)
|
||||||
|
eventTypeName := strings.TrimPrefix(addHanlder.In(0).String(), "*")
|
||||||
|
|
||||||
|
jsName := mapper.MethodName(appType, method)
|
||||||
|
result.WriteString("/** @group PocketBase */")
|
||||||
|
result.WriteString("declare function ")
|
||||||
|
result.WriteString(jsName)
|
||||||
|
result.WriteString("(handler: (e: ")
|
||||||
|
result.WriteString(eventTypeName)
|
||||||
|
result.WriteString(") => void")
|
||||||
|
if withTags {
|
||||||
|
result.WriteString(", ...tags: string[]")
|
||||||
|
}
|
||||||
|
result.WriteString("): void")
|
||||||
|
result.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -18,7 +18,6 @@ import (
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/dop251/goja_nodejs/console"
|
"github.com/dop251/goja_nodejs/console"
|
||||||
"github.com/dop251/goja_nodejs/eventloop"
|
|
||||||
"github.com/dop251/goja_nodejs/process"
|
"github.com/dop251/goja_nodejs/process"
|
||||||
"github.com/dop251/goja_nodejs/require"
|
"github.com/dop251/goja_nodejs/require"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
|
@ -49,6 +48,13 @@ type Config struct {
|
||||||
// If not set it fallbacks to a relative "pb_data/../pb_hooks" directory.
|
// If not set it fallbacks to a relative "pb_data/../pb_hooks" directory.
|
||||||
HooksDir string
|
HooksDir string
|
||||||
|
|
||||||
|
// HooksPoolSize specifies how many goja.Runtime instances to preinit
|
||||||
|
// and keep for the JS app hooks gorotines execution.
|
||||||
|
//
|
||||||
|
// Zero or negative value means that it will create a new goja.Runtime
|
||||||
|
// on every fired goroutine.
|
||||||
|
HooksPoolSize int
|
||||||
|
|
||||||
// MigrationsDir specifies the JS migrations directory.
|
// MigrationsDir specifies the JS migrations directory.
|
||||||
//
|
//
|
||||||
// If not set it fallbacks to a relative "pb_data/../pb_migrations" directory.
|
// If not set it fallbacks to a relative "pb_data/../pb_migrations" directory.
|
||||||
|
@ -172,11 +178,10 @@ func (p *plugin) registerHooks() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registry := new(require.Registry) // this can be shared by multiple runtimes
|
// this is safe to be shared across multiple vms
|
||||||
|
registry := new(require.Registry)
|
||||||
|
|
||||||
loop := eventloop.NewEventLoop()
|
sharedBinds := func(vm *goja.Runtime) {
|
||||||
|
|
||||||
loop.Run(func(vm *goja.Runtime) {
|
|
||||||
registry.Enable(vm)
|
registry.Enable(vm)
|
||||||
console.Enable(vm)
|
console.Enable(vm)
|
||||||
process.Enable(vm)
|
process.Enable(vm)
|
||||||
|
@ -186,26 +191,35 @@ func (p *plugin) registerHooks() error {
|
||||||
securityBinds(vm)
|
securityBinds(vm)
|
||||||
formsBinds(vm)
|
formsBinds(vm)
|
||||||
apisBinds(vm)
|
apisBinds(vm)
|
||||||
|
|
||||||
vm.Set("$app", p.app)
|
vm.Set("$app", p.app)
|
||||||
|
}
|
||||||
|
|
||||||
for file, content := range files {
|
// initiliaze the executor vms
|
||||||
_, err := vm.RunString(string(content))
|
executors := newPool(p.config.HooksPoolSize, func() *goja.Runtime {
|
||||||
if err != nil {
|
executor := goja.New()
|
||||||
if p.config.HooksWatch {
|
sharedBinds(executor)
|
||||||
color.Red("Failed to execute %s: %v", file, err)
|
return executor
|
||||||
} else {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
loop.Start()
|
// initialize the loader vm
|
||||||
|
loader := goja.New()
|
||||||
|
sharedBinds(loader)
|
||||||
|
hooksBinds(p.app, loader, executors)
|
||||||
|
cronBinds(p.app, loader, executors)
|
||||||
|
routerBinds(p.app, loader, executors)
|
||||||
|
|
||||||
|
for file, content := range files {
|
||||||
|
_, err := loader.RunString(string(content))
|
||||||
|
if err != nil {
|
||||||
|
if p.config.HooksWatch {
|
||||||
|
color.Red("Failed to execute %s: %v", file, err)
|
||||||
|
} else {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p.app.OnTerminate().Add(func(e *core.TerminateEvent) error {
|
p.app.OnTerminate().Add(func(e *core.TerminateEvent) error {
|
||||||
loop.StopNoWait()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -243,11 +257,12 @@ func (p *plugin) watchHooks() error {
|
||||||
|
|
||||||
// start listening for events.
|
// start listening for events.
|
||||||
go func() {
|
go func() {
|
||||||
|
defer stopDebounceTimer()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case event, ok := <-watcher.Events:
|
case event, ok := <-watcher.Events:
|
||||||
if !ok {
|
if !ok {
|
||||||
stopDebounceTimer()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,7 +281,6 @@ func (p *plugin) watchHooks() error {
|
||||||
})
|
})
|
||||||
case err, ok := <-watcher.Errors:
|
case err, ok := <-watcher.Errors:
|
||||||
if !ok {
|
if !ok {
|
||||||
stopDebounceTimer()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
color.Red("Watch error:", err)
|
color.Red("Watch error:", err)
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
package jsvm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
)
|
||||||
|
|
||||||
|
type poolItem struct {
|
||||||
|
mux sync.Mutex
|
||||||
|
busy bool
|
||||||
|
vm *goja.Runtime
|
||||||
|
}
|
||||||
|
|
||||||
|
type vmsPool struct {
|
||||||
|
mux sync.RWMutex
|
||||||
|
factory func() *goja.Runtime
|
||||||
|
items []*poolItem
|
||||||
|
}
|
||||||
|
|
||||||
|
// newPool creates a new pool with pre-warmed vms generated from the specified factory.
|
||||||
|
func newPool(size int, factory func() *goja.Runtime) *vmsPool {
|
||||||
|
pool := &vmsPool{
|
||||||
|
factory: factory,
|
||||||
|
items: make([]*poolItem, size),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
vm := pool.factory()
|
||||||
|
pool.items[i] = &poolItem{vm: vm}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// run executes "call" with a vm created from the pool
|
||||||
|
// (either from the buffer or a new one if all buffered vms are busy)
|
||||||
|
func (p *vmsPool) run(call func(vm *goja.Runtime) error) error {
|
||||||
|
p.mux.RLock()
|
||||||
|
|
||||||
|
// try to find a free item
|
||||||
|
var freeItem *poolItem
|
||||||
|
for _, item := range p.items {
|
||||||
|
item.mux.Lock()
|
||||||
|
if item.busy {
|
||||||
|
item.mux.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item.busy = true
|
||||||
|
item.mux.Unlock()
|
||||||
|
freeItem = item
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mux.RUnlock()
|
||||||
|
|
||||||
|
// create a new one-off item if of all of the pool items are currently busy
|
||||||
|
//
|
||||||
|
// note: if turned out not efficient we may change this in the future
|
||||||
|
// by adding the created item in the pool with some timer for removal
|
||||||
|
if freeItem == nil {
|
||||||
|
return call(p.factory())
|
||||||
|
}
|
||||||
|
|
||||||
|
execErr := call(freeItem.vm)
|
||||||
|
|
||||||
|
// "free" the vm
|
||||||
|
freeItem.mux.Lock()
|
||||||
|
freeItem.busy = false
|
||||||
|
freeItem.mux.Unlock()
|
||||||
|
|
||||||
|
return execErr
|
||||||
|
}
|
Loading…
Reference in New Issue