(no tests) updated jsvm bindings
This commit is contained in:
		
							parent
							
								
									5e37c90dde
								
							
						
					
					
						commit
						13d96e793b
					
				| 
						 | 
				
			
			@ -2,26 +2,212 @@ package jsvm
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/dop251/goja"
 | 
			
		||||
	validation "github.com/go-ozzo/ozzo-validation/v4"
 | 
			
		||||
	"github.com/labstack/echo/v5"
 | 
			
		||||
	"github.com/pocketbase/dbx"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/apis"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/core"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/daos"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/forms"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/models"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/models/schema"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/tokens"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/tools/cron"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/tools/filesystem"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/tools/hook"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/tools/inflector"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/tools/list"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/tools/mailer"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/tools/security"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/tools/types"
 | 
			
		||||
	"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) {
 | 
			
		||||
	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)
 | 
			
		||||
		st := reflect.SliceOf(mt)
 | 
			
		||||
		elem := reflect.New(st).Elem()
 | 
			
		||||
| 
						 | 
				
			
			@ -147,6 +333,8 @@ func baseBinds(vm *goja.Runtime) {
 | 
			
		|||
 | 
			
		||||
		return instanceValue
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	vm.Set("$stopPropagation", hook.StopPropagation)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func dbxBinds(vm *goja.Runtime) {
 | 
			
		||||
| 
						 | 
				
			
			@ -253,11 +441,6 @@ func apisBinds(vm *goja.Runtime) {
 | 
			
		|||
	obj := vm.NewObject()
 | 
			
		||||
	vm.Set("$apis", obj)
 | 
			
		||||
 | 
			
		||||
	vm.Set("Route", func(call goja.ConstructorCall) *goja.Object {
 | 
			
		||||
		instance := &echo.Route{}
 | 
			
		||||
		return structConstructor(vm, call, instance)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// middlewares
 | 
			
		||||
	obj.Set("requireRecordAuth", apis.RequireRecordAuth)
 | 
			
		||||
	obj.Set("requireAdminAuth", apis.RequireAdminAuth)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,7 +38,7 @@ func TestBaseBindsCount(t *testing.T) {
 | 
			
		|||
	vm := goja.New()
 | 
			
		||||
	baseBinds(vm)
 | 
			
		||||
 | 
			
		||||
	testBindsCount(vm, "this", 14, t)
 | 
			
		||||
	testBindsCount(vm, "this", 15, t)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestBaseBindsRecord(t *testing.T) {
 | 
			
		||||
| 
						 | 
				
			
			@ -682,48 +682,10 @@ func TestApisBindsCount(t *testing.T) {
 | 
			
		|||
	vm := goja.New()
 | 
			
		||||
	apisBinds(vm)
 | 
			
		||||
 | 
			
		||||
	testBindsCount(vm, "this", 7, t)
 | 
			
		||||
	testBindsCount(vm, "this", 6, 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) {
 | 
			
		||||
	app, _ := tests.NewTestApp()
 | 
			
		||||
	defer app.Cleanup()
 | 
			
		||||
| 
						 | 
				
			
			@ -843,7 +805,7 @@ func TestLoadingArrayOf(t *testing.T) {
 | 
			
		|||
	vm.Set("$app", app)
 | 
			
		||||
 | 
			
		||||
	_, err := vm.RunString(`
 | 
			
		||||
		let result = $arrayOf(new DynamicModel({
 | 
			
		||||
		let result = arrayOf(new DynamicModel({
 | 
			
		||||
			id:   "",
 | 
			
		||||
			text: "",
 | 
			
		||||
		}))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,16 +4,96 @@ import (
 | 
			
		|||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/pocketbase/pocketbase/core"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/plugins/jsvm"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/tools/list"
 | 
			
		||||
	"github.com/pocketbase/tygoja"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
// -------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
 * available in each .pb.js file.
 | 
			
		||||
| 
						 | 
				
			
			@ -21,23 +101,23 @@ const heading = `
 | 
			
		|||
 * @namespace
 | 
			
		||||
 * @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.
 | 
			
		||||
 *
 | 
			
		||||
 * Example:
 | 
			
		||||
 *
 | 
			
		||||
 * ` + "```" + `js
 | 
			
		||||
 * const records = $arrayOf(new Record)
 | 
			
		||||
 * const records = arrayOf(new Record)
 | 
			
		||||
 *
 | 
			
		||||
 * $app.dao().recordQuery(collection).limit(10).all(records)
 | 
			
		||||
 * ` + "```" + `
 | 
			
		||||
 *
 | 
			
		||||
 * @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.
 | 
			
		||||
| 
						 | 
				
			
			@ -498,27 +578,6 @@ declare class TestS3FilesystemForm implements forms.TestS3Filesystem {
 | 
			
		|||
// 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
 | 
			
		||||
/**
 | 
			
		||||
 * @inheritDoc
 | 
			
		||||
| 
						 | 
				
			
			@ -594,7 +653,7 @@ declare namespace $apis {
 | 
			
		|||
/**
 | 
			
		||||
 * 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
 | 
			
		||||
 */
 | 
			
		||||
| 
						 | 
				
			
			@ -604,8 +663,10 @@ declare function migrate(
 | 
			
		|||
): void;
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
var mapper = &jsvm.FieldMapper{}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	mapper := &jsvm.FieldMapper{}
 | 
			
		||||
	declarations := heading + hooksDeclarations()
 | 
			
		||||
 | 
			
		||||
	gen := tygoja.New(tygoja.Config{
 | 
			
		||||
		Packages: map[string][]string{
 | 
			
		||||
| 
						 | 
				
			
			@ -626,7 +687,7 @@ func main() {
 | 
			
		|||
		},
 | 
			
		||||
		Indent:               " ", // use only a single space to reduce slight the size
 | 
			
		||||
		WithPackageFunctions: true,
 | 
			
		||||
		Heading:              heading,
 | 
			
		||||
		Heading:              declarations,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	result, err := gen.Generate()
 | 
			
		||||
| 
						 | 
				
			
			@ -638,3 +699,45 @@ func main() {
 | 
			
		|||
		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_nodejs/console"
 | 
			
		||||
	"github.com/dop251/goja_nodejs/eventloop"
 | 
			
		||||
	"github.com/dop251/goja_nodejs/process"
 | 
			
		||||
	"github.com/dop251/goja_nodejs/require"
 | 
			
		||||
	"github.com/fatih/color"
 | 
			
		||||
| 
						 | 
				
			
			@ -49,6 +48,13 @@ type Config struct {
 | 
			
		|||
	// If not set it fallbacks to a relative "pb_data/../pb_hooks" directory.
 | 
			
		||||
	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.
 | 
			
		||||
	//
 | 
			
		||||
	// 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()
 | 
			
		||||
 | 
			
		||||
	loop.Run(func(vm *goja.Runtime) {
 | 
			
		||||
	sharedBinds := func(vm *goja.Runtime) {
 | 
			
		||||
		registry.Enable(vm)
 | 
			
		||||
		console.Enable(vm)
 | 
			
		||||
		process.Enable(vm)
 | 
			
		||||
| 
						 | 
				
			
			@ -186,11 +191,25 @@ func (p *plugin) registerHooks() error {
 | 
			
		|||
		securityBinds(vm)
 | 
			
		||||
		formsBinds(vm)
 | 
			
		||||
		apisBinds(vm)
 | 
			
		||||
 | 
			
		||||
		vm.Set("$app", p.app)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// initiliaze the executor vms
 | 
			
		||||
	executors := newPool(p.config.HooksPoolSize, func() *goja.Runtime {
 | 
			
		||||
		executor := goja.New()
 | 
			
		||||
		sharedBinds(executor)
 | 
			
		||||
		return executor
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// 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 := vm.RunString(string(content))
 | 
			
		||||
		_, err := loader.RunString(string(content))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if p.config.HooksWatch {
 | 
			
		||||
				color.Red("Failed to execute %s: %v", file, err)
 | 
			
		||||
| 
						 | 
				
			
			@ -199,13 +218,8 @@ func (p *plugin) registerHooks() error {
 | 
			
		|||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	loop.Start()
 | 
			
		||||
 | 
			
		||||
	p.app.OnTerminate().Add(func(e *core.TerminateEvent) error {
 | 
			
		||||
		loop.StopNoWait()
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -243,11 +257,12 @@ func (p *plugin) watchHooks() error {
 | 
			
		|||
 | 
			
		||||
	// start listening for events.
 | 
			
		||||
	go func() {
 | 
			
		||||
		defer stopDebounceTimer()
 | 
			
		||||
 | 
			
		||||
		for {
 | 
			
		||||
			select {
 | 
			
		||||
			case event, ok := <-watcher.Events:
 | 
			
		||||
				if !ok {
 | 
			
		||||
					stopDebounceTimer()
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -266,7 +281,6 @@ func (p *plugin) watchHooks() error {
 | 
			
		|||
				})
 | 
			
		||||
			case err, ok := <-watcher.Errors:
 | 
			
		||||
				if !ok {
 | 
			
		||||
					stopDebounceTimer()
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				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