merge v0.23.0-rc changes

This commit is contained in:
Gani Georgiev
2024-09-29 19:23:19 +03:00
parent ad92992324
commit 844f18cac3
753 changed files with 85141 additions and 63396 deletions
+1
View File
@@ -252,6 +252,7 @@ func (p *plugin) update(withBackup bool) error {
fmt.Print("\n")
color.Cyan("Here is a list with some of the %s changes:", latest.Tag)
// remove the update command note to avoid "stuttering"
// (@todo consider moving to a config option)
releaseNotes := strings.TrimSpace(strings.Replace(latest.Body, "> _To update the prebuilt executable you can run `./"+p.config.ArchiveExecutable+" update`._", "", 1))
color.Cyan(releaseNotes)
fmt.Print("\n")
+7 -5
View File
@@ -26,11 +26,13 @@ func TestCompareVersions(t *testing.T) {
{"3.2.4", "3.2.3", -1},
}
for i, s := range scenarios {
result := compareVersions(s.a, s.b)
for _, s := range scenarios {
t.Run(s.a+"VS"+s.b, func(t *testing.T) {
result := compareVersions(s.a, s.b)
if result != s.expected {
t.Fatalf("[%d] Expected %q vs %q to result in %d, got %d", i, s.a, s.b, s.expected, result)
}
if result != s.expected {
t.Fatalf("Expected %q vs %q to result in %d, got %d", s.a, s.b, s.expected, result)
}
})
}
}
+263 -190
View File
@@ -12,29 +12,23 @@ import (
"os/exec"
"path/filepath"
"reflect"
"slices"
"strings"
"time"
"github.com/dop251/goja"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
"github.com/golang-jwt/jwt/v4"
"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/mails"
"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/rest"
"github.com/pocketbase/pocketbase/tools/router"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/pocketbase/pocketbase/tools/subscriptions"
"github.com/pocketbase/pocketbase/tools/types"
@@ -49,11 +43,11 @@ func hooksBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
appType := reflect.TypeOf(app)
appValue := reflect.ValueOf(app)
totalMethods := appType.NumMethod()
excludeHooks := []string{"OnBeforeServe"}
excludeHooks := []string{"OnServe"}
for i := 0; i < totalMethods; i++ {
method := appType.Method(i)
if !strings.HasPrefix(method.Name, "On") || list.ExistInSlice(method.Name, excludeHooks) {
if !strings.HasPrefix(method.Name, "On") || slices.Contains(excludeHooks, method.Name) {
continue // not a hook or excluded
}
@@ -69,9 +63,9 @@ func hooksBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
}
hookInstance := appValue.MethodByName(method.Name).Call(tagsAsValues)[0]
addFunc := hookInstance.MethodByName("Add")
hookBindFunc := hookInstance.MethodByName("BindFunc")
handlerType := addFunc.Type().In(0)
handlerType := hookBindFunc.Type().In(0)
handler := reflect.MakeFunc(handlerType, func(args []reflect.Value) (results []reflect.Value) {
handlerArgs := make([]any, len(args))
@@ -84,15 +78,10 @@ func hooksBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
res, err := executor.RunProgram(pr)
executor.Set("__args", goja.Undefined())
// check for returned error or false
// check for returned error value
if res != nil {
switch v := res.Export().(type) {
case error:
return v
case bool:
if !v {
return hook.StopPropagation
}
if resErr, ok := res.Export().(error); ok {
return resErr
}
}
@@ -103,20 +92,16 @@ func hooksBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
})
// register the wrapped hook handler
addFunc.Call([]reflect.Value{handler})
hookBindFunc.Call([]reflect.Value{handler})
})
}
}
func cronBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
scheduler := cron.New()
var wasServeTriggered bool
loader.Set("cronAdd", func(jobId, cronExpr, handler string) {
pr := goja.MustCompile("", "{("+handler+").apply(undefined)}", true)
err := scheduler.Add(jobId, cronExpr, func() {
err := app.Cron().Add(jobId, cronExpr, func() {
err := executors.run(func(executor *goja.Runtime) error {
_, err := executor.RunProgram(pr)
return err
@@ -133,32 +118,29 @@ func cronBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
if err != nil {
panic("[cronAdd] failed to register cron job " + jobId + ": " + err.Error())
}
// start the ticker (if not already)
if wasServeTriggered && scheduler.Total() > 0 && !scheduler.HasStarted() {
scheduler.Start()
}
})
// note: it is not necessary needed but it is here for consistency
loader.Set("cronRemove", func(jobId string) {
scheduler.Remove(jobId)
// stop the ticker if there are no other jobs
if scheduler.Total() == 0 {
scheduler.Stop()
}
app.Cron().Remove(jobId)
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// start the ticker (if not already)
if scheduler.Total() > 0 && !scheduler.HasStarted() {
scheduler.Start()
}
// register the removal helper also in the executors to allow removing cron jobs from everywhere
oldFactory := executors.factory
executors.factory = func() *goja.Runtime {
vm := oldFactory()
wasServeTriggered = true
vm.Set("cronRemove", func(jobId string) {
app.Cron().Remove(jobId)
})
return nil
})
return vm
}
for _, item := range executors.items {
item.vm.Set("cronRemove", func(jobId string) {
app.Cron().Remove(jobId)
})
}
}
func routerBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
@@ -168,15 +150,15 @@ func routerBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
panic("[routerAdd] failed to wrap middlewares: " + err.Error())
}
wrappedHandler, err := wrapHandler(executors, handler)
wrappedHandler, err := wrapHandlerFunc(executors, handler)
if err != nil {
panic("[routerAdd] failed to wrap handler: " + err.Error())
}
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
e.Router.Add(strings.ToUpper(method), path, wrappedHandler, wrappedMiddlewares...)
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
e.Router.Route(strings.ToUpper(method), path, wrappedHandler).Bind(wrappedMiddlewares...)
return nil
return e.Next()
})
})
@@ -186,40 +168,28 @@ func routerBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
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
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
e.Router.Bind(wrappedMiddlewares...)
return e.Next()
})
})
}
func wrapHandler(executors *vmsPool, handler goja.Value) (echo.HandlerFunc, error) {
func wrapHandlerFunc(executors *vmsPool, handler goja.Value) (hook.HandlerFunc[*core.RequestEvent], error) {
if handler == nil {
return nil, errors.New("handler must be non-nil")
}
switch h := handler.Export().(type) {
case echo.HandlerFunc:
// "native" handler - no need to wrap
case hook.HandlerFunc[*core.RequestEvent]:
// "native" handler func - no need to wrap
return h, nil
case func(goja.FunctionCall) goja.Value, string:
pr := goja.MustCompile("", "{("+handler.String()+").apply(undefined, __args)}", true)
wrappedHandler := func(c echo.Context) error {
wrappedHandler := func(e *core.RequestEvent) error {
return executors.run(func(executor *goja.Runtime) error {
executor.Set("__args", []any{c})
executor.Set("__args", []any{e})
res, err := executor.RunProgram(pr)
executor.Set("__args", goja.Undefined())
@@ -240,29 +210,44 @@ func wrapHandler(executors *vmsPool, handler goja.Value) (echo.HandlerFunc, erro
}
}
func wrapMiddlewares(executors *vmsPool, rawMiddlewares ...goja.Value) ([]echo.MiddlewareFunc, error) {
wrappedMiddlewares := make([]echo.MiddlewareFunc, len(rawMiddlewares))
type gojaHookHandler struct {
priority int
id string
serializedFunc string
}
func wrapMiddlewares(executors *vmsPool, rawMiddlewares ...goja.Value) ([]*hook.Handler[*core.RequestEvent], error) {
wrappedMiddlewares := make([]*hook.Handler[*core.RequestEvent], len(rawMiddlewares))
for i, m := range rawMiddlewares {
if m == nil {
return nil, errors.New("middleware func must be non-nil")
return nil, errors.New("middleware must be non-nil")
}
switch v := m.Export().(type) {
case echo.MiddlewareFunc:
// "native" middleware - no need to wrap
case *hook.Handler[*core.RequestEvent]:
// "native" middleware handler - 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)
case hook.HandlerFunc[*core.RequestEvent]:
// "native" middleware func - wrap as handler
wrappedMiddlewares[i] = &hook.Handler[*core.RequestEvent]{
Func: v,
}
case *gojaHookHandler:
if v.serializedFunc == "" {
return nil, errors.New("missing or invalid Middleware function")
}
wrappedMiddlewares[i] = func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
pr := goja.MustCompile("", "{("+v.serializedFunc+").apply(undefined, __args)}", true)
wrappedMiddlewares[i] = &hook.Handler[*core.RequestEvent]{
Id: v.id,
Priority: v.priority,
Func: func(e *core.RequestEvent) error {
return executors.run(func(executor *goja.Runtime) error {
executor.Set("__args", []any{next})
executor.Set("__args2", []any{c})
executor.Set("__args", []any{e})
res, err := executor.RunProgram(pr)
executor.Set("__args", goja.Undefined())
executor.Set("__args2", goja.Undefined())
// check for returned error
if res != nil {
@@ -273,7 +258,28 @@ func wrapMiddlewares(executors *vmsPool, rawMiddlewares ...goja.Value) ([]echo.M
return err
})
}
},
}
case func(goja.FunctionCall) goja.Value, string:
pr := goja.MustCompile("", "{("+m.String()+").apply(undefined, __args)}", true)
wrappedMiddlewares[i] = &hook.Handler[*core.RequestEvent]{
Func: func(e *core.RequestEvent) error {
return executors.run(func(executor *goja.Runtime) error {
executor.Set("__args", []any{e})
res, err := executor.RunProgram(pr)
executor.Set("__args", goja.Undefined())
// check for returned error
if res != nil {
if v, ok := res.Export().(error); ok {
return v
}
}
return err
})
},
}
default:
return nil, errors.New("unsupported goja middleware type")
@@ -286,9 +292,10 @@ func wrapMiddlewares(executors *vmsPool, rawMiddlewares ...goja.Value) ([]echo.M
func baseBinds(vm *goja.Runtime) {
vm.SetFieldNameMapper(FieldMapper{})
// deprecated: use toString
vm.Set("readerToString", func(r io.Reader, maxBytes int) (string, error) {
if maxBytes == 0 {
maxBytes = rest.DefaultMaxMemory
maxBytes = router.DefaultMaxMemory
}
limitReader := io.LimitReader(r, int64(maxBytes))
@@ -301,6 +308,34 @@ func baseBinds(vm *goja.Runtime) {
return string(bodyBytes), nil
})
vm.Set("toString", func(raw any, maxReaderBytes int) (string, error) {
switch v := raw.(type) {
case io.Reader:
if maxReaderBytes == 0 {
maxReaderBytes = router.DefaultMaxMemory
}
limitReader := io.LimitReader(v, int64(maxReaderBytes))
bodyBytes, readErr := io.ReadAll(limitReader)
if readErr != nil {
return "", readErr
}
return string(bodyBytes), nil
default:
str, err := cast.ToStringE(v)
if err == nil {
return str, nil
}
// as a last attempt try to json encode the value
rawBytes, _ := json.Marshal(raw)
return string(rawBytes), nil
}
})
vm.Set("sleep", func(milliseconds int64) {
time.Sleep(time.Duration(milliseconds) * time.Millisecond)
})
@@ -313,6 +348,15 @@ func baseBinds(vm *goja.Runtime) {
return elem.Addr().Interface()
})
vm.Set("unmarshal", func(data, dst any) error {
raw, err := json.Marshal(data)
if err != nil {
return err
}
return json.Unmarshal(raw, &dst)
})
vm.Set("DynamicModel", func(call goja.ConstructorCall) *goja.Object {
shape, ok := call.Argument(0).Export().(map[string]any)
if !ok || len(shape) == 0 {
@@ -327,17 +371,17 @@ func baseBinds(vm *goja.Runtime) {
})
vm.Set("Record", func(call goja.ConstructorCall) *goja.Object {
var instance *models.Record
var instance *core.Record
collection, ok := call.Argument(0).Export().(*models.Collection)
collection, ok := call.Argument(0).Export().(*core.Collection)
if ok {
instance = models.NewRecord(collection)
instance = core.NewRecord(collection)
data, ok := call.Argument(1).Export().(map[string]any)
if ok {
instance.Load(data)
}
} else {
instance = &models.Record{}
instance = &core.Record{}
}
instanceValue := vm.ToValue(instance).(*goja.Object)
@@ -347,24 +391,91 @@ func baseBinds(vm *goja.Runtime) {
})
vm.Set("Collection", func(call goja.ConstructorCall) *goja.Object {
instance := &models.Collection{}
instance := &core.Collection{}
return structConstructorUnmarshal(vm, call, instance)
})
registerFactoryAsConstructor(vm, "BaseCollection", core.NewBaseCollection)
registerFactoryAsConstructor(vm, "AuthCollection", core.NewAuthCollection)
registerFactoryAsConstructor(vm, "ViewCollection", core.NewViewCollection)
vm.Set("FieldsList", func(call goja.ConstructorCall) *goja.Object {
instance := &core.FieldsList{}
return structConstructorUnmarshal(vm, call, instance)
})
vm.Set("Admin", func(call goja.ConstructorCall) *goja.Object {
instance := &models.Admin{}
return structConstructorUnmarshal(vm, call, instance)
})
// fields
// ---
vm.Set("Field", func(call goja.ConstructorCall) *goja.Object {
data, _ := call.Argument(0).Export().(map[string]any)
rawDataSlice, _ := json.Marshal([]any{data})
vm.Set("Schema", func(call goja.ConstructorCall) *goja.Object {
instance := &schema.Schema{}
return structConstructorUnmarshal(vm, call, instance)
})
fieldsList := core.NewFieldsList()
_ = fieldsList.UnmarshalJSON(rawDataSlice)
vm.Set("SchemaField", func(call goja.ConstructorCall) *goja.Object {
instance := &schema.SchemaField{}
if len(fieldsList) == 0 {
return nil
}
field := fieldsList[0]
fieldValue := vm.ToValue(field).(*goja.Object)
fieldValue.SetPrototype(call.This.Prototype())
return fieldValue
})
vm.Set("NumberField", func(call goja.ConstructorCall) *goja.Object {
instance := &core.NumberField{}
return structConstructorUnmarshal(vm, call, instance)
})
vm.Set("BoolField", func(call goja.ConstructorCall) *goja.Object {
instance := &core.BoolField{}
return structConstructorUnmarshal(vm, call, instance)
})
vm.Set("TextField", func(call goja.ConstructorCall) *goja.Object {
instance := &core.TextField{}
return structConstructorUnmarshal(vm, call, instance)
})
vm.Set("URLField", func(call goja.ConstructorCall) *goja.Object {
instance := &core.URLField{}
return structConstructorUnmarshal(vm, call, instance)
})
vm.Set("EmailField", func(call goja.ConstructorCall) *goja.Object {
instance := &core.EmailField{}
return structConstructorUnmarshal(vm, call, instance)
})
vm.Set("EditorField", func(call goja.ConstructorCall) *goja.Object {
instance := &core.EditorField{}
return structConstructorUnmarshal(vm, call, instance)
})
vm.Set("PasswordField", func(call goja.ConstructorCall) *goja.Object {
instance := &core.PasswordField{}
return structConstructorUnmarshal(vm, call, instance)
})
vm.Set("DateField", func(call goja.ConstructorCall) *goja.Object {
instance := &core.DateField{}
return structConstructorUnmarshal(vm, call, instance)
})
vm.Set("AutodateField", func(call goja.ConstructorCall) *goja.Object {
instance := &core.AutodateField{}
return structConstructorUnmarshal(vm, call, instance)
})
vm.Set("JSONField", func(call goja.ConstructorCall) *goja.Object {
instance := &core.JSONField{}
return structConstructorUnmarshal(vm, call, instance)
})
vm.Set("RelationField", func(call goja.ConstructorCall) *goja.Object {
instance := &core.RelationField{}
return structConstructorUnmarshal(vm, call, instance)
})
vm.Set("SelectField", func(call goja.ConstructorCall) *goja.Object {
instance := &core.SelectField{}
return structConstructorUnmarshal(vm, call, instance)
})
vm.Set("FileField", func(call goja.ConstructorCall) *goja.Object {
instance := &core.FileField{}
return structConstructorUnmarshal(vm, call, instance)
})
// ---
vm.Set("MailerMessage", func(call goja.ConstructorCall) *goja.Object {
instance := &mailer.Message{}
@@ -377,10 +488,28 @@ func baseBinds(vm *goja.Runtime) {
})
vm.Set("RequestInfo", func(call goja.ConstructorCall) *goja.Object {
instance := &models.RequestInfo{Context: models.RequestInfoContextDefault}
instance := &core.RequestInfo{Context: core.RequestInfoContextDefault}
return structConstructor(vm, call, instance)
})
// ```js
// new Middleware((e) => {
// return e.next()
// }, 100, "example_middleware")
// ```
vm.Set("Middleware", func(call goja.ConstructorCall) *goja.Object {
instance := &gojaHookHandler{}
instance.serializedFunc = call.Argument(0).String()
instance.priority = cast.ToInt(call.Argument(1).Export())
instance.id = cast.ToString(call.Argument(2).Export())
instanceValue := vm.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
})
vm.Set("DateTime", func(call goja.ConstructorCall) *goja.Object {
instance := types.NowDateTime()
@@ -406,24 +535,6 @@ func baseBinds(vm *goja.Runtime) {
return instanceValue
})
vm.Set("Dao", func(call goja.ConstructorCall) *goja.Object {
concurrentDB, _ := call.Argument(0).Export().(dbx.Builder)
if concurrentDB == nil {
panic("[Dao] missing required Dao(concurrentDB, [nonconcurrentDB]) argument")
}
nonConcurrentDB, _ := call.Argument(1).Export().(dbx.Builder)
if nonConcurrentDB == nil {
nonConcurrentDB = concurrentDB
}
instance := daos.NewMultiDB(concurrentDB, nonConcurrentDB)
instanceValue := vm.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
})
vm.Set("Cookie", func(call goja.ConstructorCall) *goja.Object {
instance := &http.Cookie{}
return structConstructor(vm, call, instance)
@@ -462,30 +573,10 @@ func mailsBinds(vm *goja.Runtime) {
obj := vm.NewObject()
vm.Set("$mails", obj)
// admin
obj.Set("sendAdminPasswordReset", mails.SendAdminPasswordReset)
// record
obj.Set("sendRecordPasswordReset", mails.SendRecordPasswordReset)
obj.Set("sendRecordVerification", mails.SendRecordVerification)
obj.Set("sendRecordChangeEmail", mails.SendRecordChangeEmail)
}
func tokensBinds(vm *goja.Runtime) {
obj := vm.NewObject()
vm.Set("$tokens", obj)
// admin
obj.Set("adminAuthToken", tokens.NewAdminAuthToken)
obj.Set("adminResetPasswordToken", tokens.NewAdminResetPasswordToken)
obj.Set("adminFileToken", tokens.NewAdminFileToken)
// record
obj.Set("recordAuthToken", tokens.NewRecordAuthToken)
obj.Set("recordVerifyToken", tokens.NewRecordVerifyToken)
obj.Set("recordResetPasswordToken", tokens.NewRecordResetPasswordToken)
obj.Set("recordChangeEmailToken", tokens.NewRecordChangeEmailToken)
obj.Set("recordFileToken", tokens.NewRecordFileToken)
obj.Set("sendRecordOTP", mails.SendRecordOTP)
}
func securityBinds(vm *goja.Runtime) {
@@ -502,6 +593,7 @@ func securityBinds(vm *goja.Runtime) {
// random
obj.Set("randomString", security.RandomString)
obj.Set("randomStringByRegex", security.RandomStringByRegex)
obj.Set("randomStringWithAlphabet", security.RandomStringWithAlphabet)
obj.Set("pseudorandomString", security.PseudorandomString)
obj.Set("pseudorandomStringWithAlphabet", security.PseudorandomStringWithAlphabet)
@@ -513,7 +605,9 @@ func securityBinds(vm *goja.Runtime) {
obj.Set("parseJWT", func(token string, verificationKey string) (map[string]any, error) {
return security.ParseJWT(token, verificationKey)
})
obj.Set("createJWT", security.NewJWT)
obj.Set("createJWT", func(payload jwt.MapClaims, signingKey string, secDuration int) (string, error) {
return security.NewJWT(payload, signingKey, time.Duration(secDuration)*time.Second)
})
// encryption
obj.Set("encrypt", security.Encrypt)
@@ -535,7 +629,7 @@ func filesystemBinds(vm *goja.Runtime) {
obj.Set("fileFromPath", filesystem.NewFileFromPath)
obj.Set("fileFromBytes", filesystem.NewFileFromBytes)
obj.Set("fileFromMultipart", filesystem.NewFileFromMultipart)
obj.Set("fileFromUrl", func(url string, secTimeout int) (*filesystem.File, error) {
obj.Set("fileFromURL", func(url string, secTimeout int) (*filesystem.File, error) {
if secTimeout == 0 {
secTimeout = 120
}
@@ -543,7 +637,7 @@ func filesystemBinds(vm *goja.Runtime) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(secTimeout)*time.Second)
defer cancel()
return filesystem.NewFileFromUrl(ctx, url)
return filesystem.NewFileFromURL(ctx, url)
})
}
@@ -592,24 +686,8 @@ func osBinds(vm *goja.Runtime) {
}
func formsBinds(vm *goja.Runtime) {
registerFactoryAsConstructor(vm, "AdminLoginForm", forms.NewAdminLogin)
registerFactoryAsConstructor(vm, "AdminPasswordResetConfirmForm", forms.NewAdminPasswordResetConfirm)
registerFactoryAsConstructor(vm, "AdminPasswordResetRequestForm", forms.NewAdminPasswordResetRequest)
registerFactoryAsConstructor(vm, "AdminUpsertForm", forms.NewAdminUpsert)
registerFactoryAsConstructor(vm, "AppleClientSecretCreateForm", forms.NewAppleClientSecretCreate)
registerFactoryAsConstructor(vm, "CollectionUpsertForm", forms.NewCollectionUpsert)
registerFactoryAsConstructor(vm, "CollectionsImportForm", forms.NewCollectionsImport)
registerFactoryAsConstructor(vm, "RealtimeSubscribeForm", forms.NewRealtimeSubscribe)
registerFactoryAsConstructor(vm, "RecordEmailChangeConfirmForm", forms.NewRecordEmailChangeConfirm)
registerFactoryAsConstructor(vm, "RecordEmailChangeRequestForm", forms.NewRecordEmailChangeRequest)
registerFactoryAsConstructor(vm, "RecordOAuth2LoginForm", forms.NewRecordOAuth2Login)
registerFactoryAsConstructor(vm, "RecordPasswordLoginForm", forms.NewRecordPasswordLogin)
registerFactoryAsConstructor(vm, "RecordPasswordResetConfirmForm", forms.NewRecordPasswordResetConfirm)
registerFactoryAsConstructor(vm, "RecordPasswordResetRequestForm", forms.NewRecordPasswordResetRequest)
registerFactoryAsConstructor(vm, "RecordUpsertForm", forms.NewRecordUpsert)
registerFactoryAsConstructor(vm, "RecordVerificationConfirmForm", forms.NewRecordVerificationConfirm)
registerFactoryAsConstructor(vm, "RecordVerificationRequestForm", forms.NewRecordVerificationRequest)
registerFactoryAsConstructor(vm, "SettingsUpsertForm", forms.NewSettingsUpsert)
registerFactoryAsConstructor(vm, "TestEmailSendForm", forms.NewTestEmailSend)
registerFactoryAsConstructor(vm, "TestS3FilesystemForm", forms.NewTestS3Filesystem)
}
@@ -618,33 +696,33 @@ func apisBinds(vm *goja.Runtime) {
obj := vm.NewObject()
vm.Set("$apis", obj)
obj.Set("staticDirectoryHandler", func(dir string, indexFallback bool) echo.HandlerFunc {
return apis.StaticDirectoryHandler(os.DirFS(dir), indexFallback)
obj.Set("static", func(dir string, indexFallback bool) hook.HandlerFunc[*core.RequestEvent] {
return apis.Static(os.DirFS(dir), indexFallback)
})
// middlewares
obj.Set("requireGuestOnly", apis.RequireGuestOnly)
obj.Set("requireRecordAuth", apis.RequireRecordAuth)
obj.Set("requireAdminAuth", apis.RequireAdminAuth)
obj.Set("requireAdminAuthOnlyIfAny", apis.RequireAdminAuthOnlyIfAny)
obj.Set("requireAdminOrRecordAuth", apis.RequireAdminOrRecordAuth)
obj.Set("requireAdminOrOwnerAuth", apis.RequireAdminOrOwnerAuth)
obj.Set("activityLogger", apis.ActivityLogger)
obj.Set("gzip", middleware.Gzip)
obj.Set("bodyLimit", middleware.BodyLimit)
obj.Set("requireAuth", apis.RequireAuth)
obj.Set("requireSuperuserAuth", apis.RequireSuperuserAuth)
obj.Set("requireSuperuserAuthOnlyIfAny", apis.RequireSuperuserAuthOnlyIfAny)
obj.Set("requireSuperuserOrOwnerAuth", apis.RequireSuperuserOrOwnerAuth)
obj.Set("skipSuccessActivityLog", apis.SkipSuccessActivityLog)
obj.Set("gzip", apis.Gzip)
obj.Set("bodyLimit", apis.BodyLimit)
// record helpers
obj.Set("requestInfo", apis.RequestInfo)
obj.Set("recordAuthResponse", apis.RecordAuthResponse)
obj.Set("enrichRecord", apis.EnrichRecord)
obj.Set("enrichRecords", apis.EnrichRecords)
// api errors
registerFactoryAsConstructor(vm, "ApiError", apis.NewApiError)
registerFactoryAsConstructor(vm, "NotFoundError", apis.NewNotFoundError)
registerFactoryAsConstructor(vm, "BadRequestError", apis.NewBadRequestError)
registerFactoryAsConstructor(vm, "ForbiddenError", apis.NewForbiddenError)
registerFactoryAsConstructor(vm, "UnauthorizedError", apis.NewUnauthorizedError)
registerFactoryAsConstructor(vm, "ApiError", router.NewApiError)
registerFactoryAsConstructor(vm, "NotFoundError", router.NewNotFoundError)
registerFactoryAsConstructor(vm, "BadRequestError", router.NewBadRequestError)
registerFactoryAsConstructor(vm, "ForbiddenError", router.NewForbiddenError)
registerFactoryAsConstructor(vm, "UnauthorizedError", router.NewUnauthorizedError)
registerFactoryAsConstructor(vm, "TooManyRequestsError", router.NewTooManyRequestsError)
registerFactoryAsConstructor(vm, "InternalServerError", router.NewInternalServerError)
}
func httpClientBinds(vm *goja.Runtime) {
@@ -661,7 +739,7 @@ func httpClientBinds(vm *goja.Runtime) {
})
type sendResult struct {
Json any `json:"json"`
JSON any `json:"json"`
Headers map[string][]string `json:"headers"`
Cookies map[string]*http.Cookie `json:"cookies"`
Raw string `json:"raw"`
@@ -727,6 +805,8 @@ func httpClientBinds(vm *goja.Runtime) {
reqBody = bytes.NewReader(encoded)
} else {
switch v := config.Body.(type) {
case io.Reader:
reqBody = v
case FormData:
body, mp, err := v.toMultipart()
if err != nil {
@@ -755,13 +835,6 @@ func httpClientBinds(vm *goja.Runtime) {
req.Header.Set("content-type", contentType)
}
// @todo consider removing during the refactoring
//
// fallback to json content-type
if req.Header.Get("content-type") == "" {
req.Header.Set("content-type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
@@ -787,12 +860,12 @@ func httpClientBinds(vm *goja.Runtime) {
if len(result.Raw) != 0 {
// try as map
result.Json = map[string]any{}
if err := json.Unmarshal(bodyRaw, &result.Json); err != nil {
result.JSON = map[string]any{}
if err := json.Unmarshal(bodyRaw, &result.JSON); err != nil {
// try as slice
result.Json = []any{}
if err := json.Unmarshal(bodyRaw, &result.Json); err != nil {
result.Json = nil
result.JSON = []any{}
if err := json.Unmarshal(bodyRaw, &result.JSON); err != nil {
result.JSON = nil
}
}
}
@@ -864,7 +937,7 @@ func structConstructor(vm *goja.Runtime, call goja.ConstructorCall, instance any
func structConstructorUnmarshal(vm *goja.Runtime, call goja.ConstructorCall, instance any) *goja.Object {
if data := call.Argument(0).Export(); data != nil {
if raw, err := json.Marshal(data); err == nil {
json.Unmarshal(raw, instance)
_ = json.Unmarshal(raw, instance)
}
}
@@ -893,13 +966,13 @@ func newDynamicModel(shape map[string]any) any {
switch kind := vt.Kind(); kind {
case reflect.Map:
raw, _ := json.Marshal(v)
newV := types.JsonMap{}
newV := types.JSONMap[any]{}
newV.Scan(raw)
v = newV
vt = reflect.TypeOf(v)
case reflect.Slice, reflect.Array:
raw, _ := json.Marshal(v)
newV := types.JsonArray[any]{}
newV := types.JSONArray[any]{}
newV.Scan(raw)
v = newV
vt = reflect.TypeOf(newV)
+315 -298
View File
File diff suppressed because it is too large Load Diff
+1 -3
View File
@@ -84,9 +84,7 @@ func (data FormData) Values() []any {
result := make([]any, 0, len(data))
for _, values := range data {
for _, v := range values {
result = append(result, v)
}
result = append(result, values...)
}
return result
File diff suppressed because it is too large Load Diff
+358 -299
View File
@@ -71,9 +71,9 @@ declare function cronRemove(jobId: string): void;
* Example:
*
* ` + "```" + `js
* routerAdd("GET", "/hello", (c) => {
* return c.json(200, {"message": "Hello!"})
* }, $apis.requireAdminOrRecordAuth())
* routerAdd("GET", "/hello", (e) => {
* return e.json(200, {"message": "Hello!"})
* }, $apis.requireAuth())
* ` + "```" + `
*
* _Note that this method is available only in pb_hooks context._
@@ -83,8 +83,8 @@ declare function cronRemove(jobId: string): void;
declare function routerAdd(
method: string,
path: string,
handler: echo.HandlerFunc,
...middlewares: Array<string|echo.MiddlewareFunc>,
handler: (e: core.RequestEvent) => void,
...middlewares: Array<string|((e: core.RequestEvent) => void)|Middleware>,
): void;
/**
@@ -94,11 +94,9 @@ declare function routerAdd(
* Example:
*
* ` + "```" + `js
* routerUse((next) => {
* return (c) => {
* console.log(c.path())
* return next(c)
* }
* routerUse((e) => {
* console.log(e.request.url.path)
* return e.next()
* })
* ` + "```" + `
*
@@ -106,34 +104,7 @@ declare function routerAdd(
*
* @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;
declare function routerUse(...middlewares: Array<string|((e: core.RequestEvent) => void)|Middleware): void;
// -------------------------------------------------------------------
// baseBinds
@@ -151,7 +122,7 @@ declare var __hooks: string
//
// See https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as
type excludeHooks<Type> = {
[Property in keyof Type as Exclude<Property, ` + "`on${string}`" + `>]: Type[Property]
[Property in keyof Type as Exclude<Property, ` + "`on${string}`" + `|'cron'>]: Type[Property]
};
// core.App without the on* hook methods
@@ -194,22 +165,34 @@ declare var $app: PocketBase
declare var $template: template.Registry
/**
* readerToString reads the content of the specified io.Reader until
* EOF or maxBytes are reached.
* This method is superseded by toString.
*
* If maxBytes is not specified it will read up to 32MB.
* @deprecated
* @group PocketBase
*/
declare function readerToString(reader: any, maxBytes?: number): string;
/**
* toString stringifies the specified value.
*
* Note that after this call the reader can't be used anymore.
* Support optional second maxBytes argument to limit the max read bytes
* when the value is a io.Reader (default to 32MB).
*
* Types that don't have explicit string representation are json serialized.
*
* Example:
*
* ` + "```" + `js
* const rawBody = readerToString(c.request().body)
* // io.Reader
* const ex1 = toString(e.request.body)
*
* // slice of bytes ("hello")
* const ex2 = toString([104 101 108 108 111])
* ` + "```" + `
*
* @group PocketBase
*/
declare function readerToString(reader: any, maxBytes?: number): string;
declare function toString(val: any, maxBytes?: number): string;
/**
* sleep pauses the current goroutine for at least the specified user duration (in ms).
@@ -244,15 +227,18 @@ declare function arrayOf<T>(model: T): Array<T>;
/**
* DynamicModel creates a new dynamic model with fields from the provided data shape.
*
* Note that in order to use 0 as double/float initialization number you have to use negative zero (` + "`-0`" + `).
*
* Example:
*
* ` + "```" + `js
* const model = new DynamicModel({
* name: ""
* age: 0,
* active: false,
* roles: [],
* meta: {}
* name: ""
* age: 0, // int64
* totalSpent: -0, // float64
* active: false,
* roles: [],
* meta: {}
* })
* ` + "```" + `
*
@@ -279,12 +265,12 @@ declare class DynamicModel {
* @group PocketBase
*/
declare const Record: {
new(collection?: models.Collection, data?: { [key:string]: any }): models.Record
new(collection?: core.Collection, data?: { [key:string]: any }): core.Record
// note: declare as "newable" const due to conflict with the Record TS utility type
}
interface Collection extends models.Collection{} // merge
interface Collection extends core.Collection{} // merge
/**
* Collection model class.
*
@@ -295,12 +281,13 @@ interface Collection extends models.Collection{} // merge
* listRule: "@request.auth.id != '' || status = 'public'",
* viewRule: "@request.auth.id != '' || status = 'public'",
* deleteRule: "@request.auth.id != ''",
* schema: [
* fields: [
* {
* name: "title",
* type: "text",
* required: true,
* options: { min: 6, max: 100 },
* min: 6,
* max: 100,
* },
* {
* name: "description",
@@ -312,44 +299,242 @@ interface Collection extends models.Collection{} // merge
*
* @group PocketBase
*/
declare class Collection implements models.Collection {
constructor(data?: Partial<models.Collection>)
declare class Collection implements core.Collection {
constructor(data?: Partial<core.Collection>)
}
interface Admin extends models.Admin{} // merge
interface BaseCollection extends core.Collection{} // merge
/**
* Admin model class.
* Alias for a "base" collection class.
*
* ` + "```" + `js
* const admin = new Admin()
* admin.email = "test@example.com"
* admin.setPassword(1234567890)
* const collection = new BaseCollection({
* name: "article",
* listRule: "@request.auth.id != '' || status = 'public'",
* viewRule: "@request.auth.id != '' || status = 'public'",
* deleteRule: "@request.auth.id != ''",
* fields: [
* {
* name: "title",
* type: "text",
* required: true,
* min: 6,
* max: 100,
* },
* {
* name: "description",
* type: "text",
* },
* ]
* })
* ` + "```" + `
*
* @group PocketBase
*/
declare class Admin implements models.Admin {
constructor(data?: Partial<models.Admin>)
declare class BaseCollection implements core.Collection {
constructor(data?: Partial<core.Collection>)
}
interface Schema extends schema.Schema{} // merge
interface AuthCollection extends core.Collection{} // merge
/**
* Schema model class, usually used to define the Collection.schema field.
* Alias for an "auth" collection class.
*
* ` + "```" + `js
* const collection = new AuthCollection({
* name: "clients",
* listRule: "@request.auth.id != '' || status = 'public'",
* viewRule: "@request.auth.id != '' || status = 'public'",
* deleteRule: "@request.auth.id != ''",
* fields: [
* {
* name: "title",
* type: "text",
* required: true,
* min: 6,
* max: 100,
* },
* {
* name: "description",
* type: "text",
* },
* ]
* })
* ` + "```" + `
*
* @group PocketBase
*/
declare class Schema implements schema.Schema {
constructor(data?: Partial<schema.Schema>)
declare class AuthCollection implements core.Collection {
constructor(data?: Partial<core.Collection>)
}
interface SchemaField extends schema.SchemaField{} // merge
interface ViewCollection extends core.Collection{} // merge
/**
* SchemaField model class, usually used as part of the Schema model.
* Alias for a "view" collection class.
*
* ` + "```" + `js
* const collection = new ViewCollection({
* name: "clients",
* listRule: "@request.auth.id != '' || status = 'public'",
* viewRule: "@request.auth.id != '' || status = 'public'",
* deleteRule: "@request.auth.id != ''",
* viewQuery: "SELECT id, title from posts",
* })
* ` + "```" + `
*
* @group PocketBase
*/
declare class SchemaField implements schema.SchemaField {
constructor(data?: Partial<schema.SchemaField>)
declare class ViewCollection implements core.Collection {
constructor(data?: Partial<core.Collection>)
}
interface FieldsList extends core.FieldsList{} // merge
/**
* FieldsList model class, usually used to define the Collection.fields.
*
* @group PocketBase
*/
declare class FieldsList implements core.FieldsList {
constructor(data?: Partial<core.FieldsList>)
}
interface Field extends core.Field{} // merge
/**
* Field model class, usually used as part of the FieldsList model.
*
* @group PocketBase
*/
declare class Field implements core.Field {
constructor(data?: Partial<core.Field>)
}
interface NumberField extends core.NumberField{} // merge
/**
* NumberField class defines a single "number" collection field.
*
* @group PocketBase
*/
declare class NumberField implements core.NumberField {
constructor(data?: Partial<core.NumberField>)
}
interface BoolField extends core.BoolField{} // merge
/**
* BoolField class defines a single "bool" collection field.
*
* @group PocketBase
*/
declare class BoolField implements core.BoolField {
constructor(data?: Partial<core.BoolField>)
}
interface TextField extends core.TextField{} // merge
/**
* TextField class defines a single "text" collection field.
*
* @group PocketBase
*/
declare class TextField implements core.TextField {
constructor(data?: Partial<core.TextField>)
}
interface URLField extends core.URLField{} // merge
/**
* URLField class defines a single "url" collection field.
*
* @group PocketBase
*/
declare class URLField implements core.URLField {
constructor(data?: Partial<core.URLField>)
}
interface EmailField extends core.EmailField{} // merge
/**
* EmailField class defines a single "email" collection field.
*
* @group PocketBase
*/
declare class EmailField implements core.EmailField {
constructor(data?: Partial<core.EmailField>)
}
interface EditorField extends core.EditorField{} // merge
/**
* EditorField class defines a single "editor" collection field.
*
* @group PocketBase
*/
declare class EditorField implements core.EditorField {
constructor(data?: Partial<core.EditorField>)
}
interface PasswordField extends core.PasswordField{} // merge
/**
* PasswordField class defines a single "password" collection field.
*
* @group PocketBase
*/
declare class PasswordField implements core.PasswordField {
constructor(data?: Partial<core.PasswordField>)
}
interface DateField extends core.DateField{} // merge
/**
* DateField class defines a single "date" collection field.
*
* @group PocketBase
*/
declare class DateField implements core.DateField {
constructor(data?: Partial<core.DateField>)
}
interface AutodateField extends core.AutodateField{} // merge
/**
* AutodateField class defines a single "autodate" collection field.
*
* @group PocketBase
*/
declare class AutodateField implements core.AutodateField {
constructor(data?: Partial<core.AutodateField>)
}
interface JSONField extends core.JSONField{} // merge
/**
* JSONField class defines a single "json" collection field.
*
* @group PocketBase
*/
declare class JSONField implements core.JSONField {
constructor(data?: Partial<core.JSONField>)
}
interface RelationField extends core.RelationField{} // merge
/**
* RelationField class defines a single "relation" collection field.
*
* @group PocketBase
*/
declare class RelationField implements core.RelationField {
constructor(data?: Partial<core.RelationField>)
}
interface SelectField extends core.SelectField{} // merge
/**
* SelectField class defines a single "select" collection field.
*
* @group PocketBase
*/
declare class SelectField implements core.SelectField {
constructor(data?: Partial<core.SelectField>)
}
interface FileField extends core.FileField{} // merge
/**
* FileField class defines a single "file" collection field.
*
* @group PocketBase
*/
declare class FileField implements core.FileField {
constructor(data?: Partial<core.FileField>)
}
interface MailerMessage extends mailer.Message{} // merge
@@ -397,31 +582,55 @@ declare class Command implements cobra.Command {
constructor(cmd?: Partial<cobra.Command>)
}
interface RequestInfo extends models.RequestInfo{} // merge
interface RequestInfo extends core.RequestInfo{} // merge
/**
* RequestInfo defines a single models.RequestInfo instance, usually used
* RequestInfo defines a single core.RequestInfo instance, usually used
* as part of various filter checks.
*
* Example:
*
* ` + "```" + `js
* const authRecord = $app.dao().findAuthRecordByEmail("users", "test@example.com")
* const authRecord = $app.findAuthRecordByEmail("users", "test@example.com")
*
* const info = new RequestInfo({
* authRecord: authRecord,
* data: {"name": 123},
* headers: {"x-token": "..."},
* auth: authRecord,
* body: {"name": 123},
* headers: {"x-token": "..."},
* })
*
* const record = $app.dao().findFirstRecordByData("articles", "slug", "hello")
* const record = $app.findFirstRecordByData("articles", "slug", "hello")
*
* const canAccess = $app.dao().canAccessRecord(record, info, "@request.auth.id != '' && @request.data.name = 123")
* const canAccess = $app.canAccessRecord(record, info, "@request.auth.id != '' && @request.body.name = 123")
* ` + "```" + `
*
* @group PocketBase
*/
declare class RequestInfo implements models.RequestInfo {
constructor(date?: Partial<models.RequestInfo>)
declare class RequestInfo implements core.RequestInfo {
constructor(info?: Partial<core.RequestInfo>)
}
/**
* Middleware defines a single request middleware handler.
*
* This class is usually used when you want to explicitly specify a priority to your custom route middleware.
*
* Example:
*
* ` + "```" + `js
* routerUse(new Middleware((e) => {
* console.log(e.request.url.path)
* return e.next()
* }, -10))
* ` + "```" + `
*
* @group PocketBase
*/
declare class Middleware {
constructor(
func: string|((e: core.RequestEvent) => void),
priority?: number,
id?: string,
)
}
interface DateTime extends types.DateTime{} // merge
@@ -457,15 +666,6 @@ declare class ValidationError implements ozzo_validation.Error {
constructor(code?: string, message?: string)
}
interface Dao extends daos.Dao{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class Dao implements daos.Dao {
constructor(concurrentDB?: dbx.Builder, nonconcurrentDB?: dbx.Builder)
}
interface Cookie extends http.Cookie{} // merge
/**
* A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an
@@ -551,44 +751,21 @@ declare namespace $dbx {
export let notBetween: dbx.notBetween
}
// -------------------------------------------------------------------
// tokensBinds
// -------------------------------------------------------------------
/**
* ` + "`" + `$tokens` + "`" + ` defines high level helpers to generate
* various admins and auth records tokens (auth, forgotten password, etc.).
*
* For more control over the generated token, you can check ` + "`" + `$security` + "`" + `.
*
* @group PocketBase
*/
declare namespace $tokens {
let adminAuthToken: tokens.newAdminAuthToken
let adminResetPasswordToken: tokens.newAdminResetPasswordToken
let adminFileToken: tokens.newAdminFileToken
let recordAuthToken: tokens.newRecordAuthToken
let recordVerifyToken: tokens.newRecordVerifyToken
let recordResetPasswordToken: tokens.newRecordResetPasswordToken
let recordChangeEmailToken: tokens.newRecordChangeEmailToken
let recordFileToken: tokens.newRecordFileToken
}
// -------------------------------------------------------------------
// mailsBinds
// -------------------------------------------------------------------
/**
* ` + "`" + `$mails` + "`" + ` defines helpers to send common
* admins and auth records emails like verification, password reset, etc.
* auth records emails like verification, password reset, etc.
*
* @group PocketBase
*/
declare namespace $mails {
let sendAdminPasswordReset: mails.sendAdminPasswordReset
let sendRecordPasswordReset: mails.sendRecordPasswordReset
let sendRecordVerification: mails.sendRecordVerification
let sendRecordChangeEmail: mails.sendRecordChangeEmail
let sendRecordOTP: mails.sendRecordOTP
}
// -------------------------------------------------------------------
@@ -604,6 +781,7 @@ declare namespace $mails {
declare namespace $security {
let randomString: security.randomString
let randomStringWithAlphabet: security.randomStringWithAlphabet
let randomStringByRegex: security.randomStringByRegex
let pseudorandomString: security.pseudorandomString
let pseudorandomStringWithAlphabet: security.pseudorandomStringWithAlphabet
let encrypt: security.encrypt
@@ -614,7 +792,11 @@ declare namespace $security {
let md5: security.md5
let sha256: security.sha256
let sha512: security.sha512
let createJWT: security.newJWT
/**
* {@inheritDoc security.newJWT}
*/
export function createJWT(payload: { [key:string]: any }, signingKey: string, secDuration: number): string
/**
* {@inheritDoc security.parseUnverifiedJWT}
@@ -739,42 +921,6 @@ declare namespace $os {
// formsBinds
// -------------------------------------------------------------------
interface AdminLoginForm extends forms.AdminLogin{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class AdminLoginForm implements forms.AdminLogin {
constructor(app: CoreApp)
}
interface AdminPasswordResetConfirmForm extends forms.AdminPasswordResetConfirm{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class AdminPasswordResetConfirmForm implements forms.AdminPasswordResetConfirm {
constructor(app: CoreApp)
}
interface AdminPasswordResetRequestForm extends forms.AdminPasswordResetRequest{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class AdminPasswordResetRequestForm implements forms.AdminPasswordResetRequest {
constructor(app: CoreApp)
}
interface AdminUpsertForm extends forms.AdminUpsert{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class AdminUpsertForm implements forms.AdminUpsert {
constructor(app: CoreApp, admin: models.Admin)
}
interface AppleClientSecretCreateForm extends forms.AppleClientSecretCreate{} // merge
/**
* @inheritDoc
@@ -784,119 +930,13 @@ declare class AppleClientSecretCreateForm implements forms.AppleClientSecretCrea
constructor(app: CoreApp)
}
interface CollectionUpsertForm extends forms.CollectionUpsert{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class CollectionUpsertForm implements forms.CollectionUpsert {
constructor(app: CoreApp, collection: models.Collection)
}
interface CollectionsImportForm extends forms.CollectionsImport{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class CollectionsImportForm implements forms.CollectionsImport {
constructor(app: CoreApp)
}
interface RealtimeSubscribeForm extends forms.RealtimeSubscribe{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class RealtimeSubscribeForm implements forms.RealtimeSubscribe {}
interface RecordEmailChangeConfirmForm extends forms.RecordEmailChangeConfirm{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class RecordEmailChangeConfirmForm implements forms.RecordEmailChangeConfirm {
constructor(app: CoreApp, collection: models.Collection)
}
interface RecordEmailChangeRequestForm extends forms.RecordEmailChangeRequest{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class RecordEmailChangeRequestForm implements forms.RecordEmailChangeRequest {
constructor(app: CoreApp, record: models.Record)
}
interface RecordOAuth2LoginForm extends forms.RecordOAuth2Login{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class RecordOAuth2LoginForm implements forms.RecordOAuth2Login {
constructor(app: CoreApp, collection: models.Collection, optAuthRecord?: models.Record)
}
interface RecordPasswordLoginForm extends forms.RecordPasswordLogin{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class RecordPasswordLoginForm implements forms.RecordPasswordLogin {
constructor(app: CoreApp, collection: models.Collection)
}
interface RecordPasswordResetConfirmForm extends forms.RecordPasswordResetConfirm{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class RecordPasswordResetConfirmForm implements forms.RecordPasswordResetConfirm {
constructor(app: CoreApp, collection: models.Collection)
}
interface RecordPasswordResetRequestForm extends forms.RecordPasswordResetRequest{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class RecordPasswordResetRequestForm implements forms.RecordPasswordResetRequest {
constructor(app: CoreApp, collection: models.Collection)
}
interface RecordUpsertForm extends forms.RecordUpsert{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class RecordUpsertForm implements forms.RecordUpsert {
constructor(app: CoreApp, record: models.Record)
}
interface RecordVerificationConfirmForm extends forms.RecordVerificationConfirm{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class RecordVerificationConfirmForm implements forms.RecordVerificationConfirm {
constructor(app: CoreApp, collection: models.Collection)
}
interface RecordVerificationRequestForm extends forms.RecordVerificationRequest{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class RecordVerificationRequestForm implements forms.RecordVerificationRequest {
constructor(app: CoreApp, collection: models.Collection)
}
interface SettingsUpsertForm extends forms.SettingsUpsert{} // merge
/**
* @inheritDoc
* @group PocketBase
*/
declare class SettingsUpsertForm implements forms.SettingsUpsert {
constructor(app: CoreApp)
constructor(app: CoreApp, record: core.Record)
}
interface TestEmailSendForm extends forms.TestEmailSend{} // merge
@@ -971,6 +1011,26 @@ declare class UnauthorizedError implements apis.ApiError {
constructor(message?: string, data?: any)
}
interface TooManyRequestsError extends apis.ApiError{} // merge
/**
* TooManyRequestsError returns 429 ApiError.
*
* @group PocketBase
*/
declare class TooManyRequestsError implements apis.ApiError {
constructor(message?: string, data?: any)
}
interface InternalServerError extends apis.ApiError{} // merge
/**
* InternalServerError returns 429 ApiError.
*
* @group PocketBase
*/
declare class InternalServerError implements apis.ApiError {
constructor(message?: string, data?: any)
}
/**
* ` + "`" + `$apis` + "`" + ` defines commonly used PocketBase api helpers and middlewares.
*
@@ -983,21 +1043,19 @@ declare namespace $apis {
* If a file resource is missing and indexFallback is set, the request
* will be forwarded to the base index.html (useful for SPA).
*/
export function staticDirectoryHandler(dir: string, indexFallback: boolean): echo.HandlerFunc
export function static(dir: string, indexFallback: boolean): (e: core.RequestEvent) => void
let requireGuestOnly: apis.requireGuestOnly
let requireRecordAuth: apis.requireRecordAuth
let requireAdminAuth: apis.requireAdminAuth
let requireAdminAuthOnlyIfAny: apis.requireAdminAuthOnlyIfAny
let requireAdminOrRecordAuth: apis.requireAdminOrRecordAuth
let requireAdminOrOwnerAuth: apis.requireAdminOrOwnerAuth
let activityLogger: apis.activityLogger
let requestInfo: apis.requestInfo
let recordAuthResponse: apis.recordAuthResponse
let gzip: middleware.gzip
let bodyLimit: middleware.bodyLimit
let enrichRecord: apis.enrichRecord
let enrichRecords: apis.enrichRecords
let requireGuestOnly: apis.requireGuestOnly
let requireAuth: apis.requireAuth
let requireSuperuserAuth: apis.requireSuperuserAuth
let requireSuperuserAuthOnlyIfAny: apis.requireSuperuserAuthOnlyIfAny
let requireSuperuserOrOwnerAuth: apis.requireSuperuserOrOwnerAuth
let skipSuccessActivityLog: apis.skipSuccessActivityLog
let gzip: apis.gzip
let bodyLimit: apis.bodyLimit
let recordAuthResponse: apis.recordAuthResponse
let enrichRecord: apis.enrichRecord
let enrichRecords: apis.enrichRecords
}
// -------------------------------------------------------------------
@@ -1023,9 +1081,10 @@ declare namespace $http {
*
* ` + "```" + `js
* const res = $http.send({
* url: "https://example.com",
* body: JSON.stringify({"title": "test"})
* method: "post",
* method: "POST",
* url: "https://example.com",
* body: JSON.stringify({"title": "test"}),
* headers: { 'Content-Type': 'application/json' }
* })
*
* console.log(res.statusCode) // the response HTTP status code
@@ -1042,7 +1101,7 @@ declare namespace $http {
headers?: { [key:string]: string },
timeout?: number, // default to 120
// deprecated, please use body instead
// @deprecated please use body instead
data?: { [key:string]: any },
}): {
statusCode: number,
@@ -1065,8 +1124,8 @@ declare namespace $http {
* @group PocketBase
*/
declare function migrate(
up: (db: dbx.Builder) => void,
down?: (db: dbx.Builder) => void
up: (txApp: CoreApp) => void,
down?: (txApp: CoreApp) => void
): void;
`
@@ -1077,13 +1136,11 @@ func main() {
gen := tygoja.New(tygoja.Config{
Packages: map[string][]string{
"github.com/labstack/echo/v5/middleware": {"Gzip", "BodyLimit"},
"github.com/go-ozzo/ozzo-validation/v4": {"Error"},
"github.com/pocketbase/dbx": {"*"},
"github.com/pocketbase/pocketbase/tools/security": {"*"},
"github.com/pocketbase/pocketbase/tools/filesystem": {"*"},
"github.com/pocketbase/pocketbase/tools/template": {"*"},
"github.com/pocketbase/pocketbase/tokens": {"*"},
"github.com/pocketbase/pocketbase/mails": {"*"},
"github.com/pocketbase/pocketbase/apis": {"*"},
"github.com/pocketbase/pocketbase/forms": {"*"},
@@ -1099,23 +1156,25 @@ func main() {
return mapper.MethodName(nil, reflect.Method{Name: s})
},
TypeMappings: map[string]string{
"crypto.*": "any",
"acme.*": "any",
"autocert.*": "any",
"driver.*": "any",
"reflect.*": "any",
"fmt.*": "any",
"rand.*": "any",
"tls.*": "any",
"asn1.*": "any",
"pkix.*": "any",
"x509.*": "any",
"pflag.*": "any",
"flag.*": "any",
"log.*": "any",
"http.Client": "any",
"crypto.*": "any",
"acme.*": "any",
"autocert.*": "any",
"driver.*": "any",
"reflect.*": "any",
"fmt.*": "any",
"rand.*": "any",
"tls.*": "any",
"asn1.*": "any",
"pkix.*": "any",
"x509.*": "any",
"pflag.*": "any",
"flag.*": "any",
"log.*": "any",
"aws.*": "any",
"http.Client": "any",
"mail.Address": "{ address: string; name?: string; }", // prevents the LSP to complain in case no name is provided
},
Indent: " ", // use only a single space to reduce slight the size
Indent: " ", // use only a single space to reduce slightly the size
WithPackageFunctions: true,
Heading: declarations,
})
@@ -1151,7 +1210,7 @@ func main() {
func hooksDeclarations() string {
var result strings.Builder
excluded := []string{"OnBeforeServe"}
excluded := []string{"OnServe"}
appType := reflect.TypeOf(struct{ core.App }{})
totalMethods := appType.NumMethod()
@@ -1165,7 +1224,7 @@ func hooksDeclarations() string {
withTags := strings.HasPrefix(hookType.String(), "*hook.TaggedHook")
addMethod, ok := hookType.MethodByName("Add")
addMethod, ok := hookType.MethodByName("BindFunc")
if !ok {
continue
}
+37 -38
View File
@@ -27,17 +27,12 @@ import (
"github.com/dop251/goja_nodejs/require"
"github.com/fatih/color"
"github.com/fsnotify/fsnotify"
"github.com/labstack/echo/v5"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/plugins/jsvm/internal/types/generated"
"github.com/pocketbase/pocketbase/tools/template"
)
const (
typesFileName = "types.d.ts"
)
const typesFileName = "types.d.ts"
// Config defines the config options of the jsvm plugin.
type Config struct {
@@ -131,9 +126,15 @@ func Register(app core.App, config Config) error {
p.config.TypesDir = app.DataDir()
}
p.app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error {
p.app.OnBootstrap().BindFunc(func(e *core.BootstrapEvent) error {
err := e.Next()
if err != nil {
return err
}
// ensure that the user has the latest types declaration
if err := p.refreshTypesFile(); err != nil {
err = p.refreshTypesFile()
if err != nil {
color.Yellow("Unable to refresh app types file: %v", err)
}
@@ -173,14 +174,13 @@ func (p *plugin) registerMigrations() error {
process.Enable(vm)
baseBinds(vm)
dbxBinds(vm)
tokensBinds(vm)
securityBinds(vm)
osBinds(vm)
filepathBinds(vm)
httpClientBinds(vm)
vm.Set("migrate", func(up, down func(db dbx.Builder) error) {
m.AppMigrations.Register(up, down, file)
vm.Set("migrate", func(up, down func(txApp core.App) error) {
core.AppMigrations.Register(up, down, file)
})
if p.config.OnInit != nil {
@@ -238,9 +238,10 @@ func (p *plugin) registerHooks() error {
return err
}
p.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
e.Router.HTTPErrorHandler = p.normalizeServeExceptions(e.Router.HTTPErrorHandler)
return nil
p.app.OnServe().BindFunc(func(e *core.ServeEvent) error {
e.Router.BindFunc(p.normalizeServeExceptions)
return e.Next()
})
// safe to be shared across multiple vms
@@ -255,7 +256,6 @@ func (p *plugin) registerHooks() error {
baseBinds(vm)
dbxBinds(vm)
filesystemBinds(vm)
tokensBinds(vm)
securityBinds(vm)
osBinds(vm)
filepathBinds(vm)
@@ -311,32 +311,31 @@ func (p *plugin) registerHooks() error {
return nil
}
// normalizeExceptions wraps the provided error handler and returns a new one
// with extracted goja exception error value for consistency when throwing or returning errors.
func (p *plugin) normalizeServeExceptions(oldErrorHandler echo.HTTPErrorHandler) echo.HTTPErrorHandler {
return func(c echo.Context, err error) {
defer func() {
oldErrorHandler(c, err)
}()
// normalizeExceptions registers a global error handler that
// wraps the extracted goja exception error value for consistency
// when throwing or returning errors.
func (p *plugin) normalizeServeExceptions(e *core.RequestEvent) error {
err := e.Next()
if err == nil || c.Response().Committed {
return // no error or already committed
}
if err == nil || e.Written() {
return err // no error or already committed
}
jsException, ok := err.(*goja.Exception)
if !ok {
return // no exception
}
jsException, ok := err.(*goja.Exception)
if !ok {
return err // no exception
}
switch v := jsException.Value().Export().(type) {
case error:
err = v
case map[string]any: // goja.GoError
if vErr, ok := v["value"].(error); ok {
err = vErr
}
switch v := jsException.Value().Export().(type) {
case error:
err = v
case map[string]any: // goja.GoError
if vErr, ok := v["value"].(error); ok {
err = vErr
}
}
return err
}
// watchHooks initializes a hooks file watcher that will restart the
@@ -365,12 +364,12 @@ func (p *plugin) watchHooks() error {
}
}
p.app.OnTerminate().Add(func(e *core.TerminateEvent) error {
p.app.OnTerminate().BindFunc(func(e *core.TerminateEvent) error {
watcher.Close()
stopDebounceTimer()
return nil
return e.Next()
})
// start listening for events.
+70 -114
View File
@@ -10,130 +10,86 @@ import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tools/migrate"
)
const collectionsStoreKey = "migratecmd_collections"
// onCollectionChange handles the automigration snapshot generation on
// collection change event (create/update/delete).
func (p *plugin) afterCollectionChange() func(*core.ModelEvent) error {
return func(e *core.ModelEvent) error {
if e.Model.TableName() != "_collections" {
return nil // not a collection
// automigrateOnCollectionChange handles the automigration snapshot
// generation on collection change request event (create/update/delete).
func (p *plugin) automigrateOnCollectionChange(e *core.CollectionRequestEvent) error {
var err error
var old *core.Collection
if !e.Collection.IsNew() {
old, err = e.App.FindCollectionByNameOrId(e.Collection.Id)
if err != nil {
return err
}
}
// @todo replace with the OldModel when added to the ModelEvent
oldCollections, err := p.getCachedCollections()
err = e.Next()
if err != nil {
return err
}
new, err := p.app.FindCollectionByNameOrId(e.Collection.Id)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
}
// for now exclude OAuth2 configs from the migration
if old != nil && old.IsAuth() {
old.OAuth2.Providers = nil
}
if new != nil && new.IsAuth() {
new.OAuth2.Providers = nil
}
var template string
var templateErr error
if p.config.TemplateLang == TemplateLangJS {
template, templateErr = p.jsDiffTemplate(new, old)
} else {
template, templateErr = p.goDiffTemplate(new, old)
}
if templateErr != nil {
if errors.Is(templateErr, ErrEmptyTemplate) {
return nil // no changes
}
return fmt.Errorf("failed to resolve template: %w", templateErr)
}
var action string
switch {
case new == nil:
action = "deleted_" + old.Name
case old == nil:
action = "created_" + new.Name
default:
action = "updated_" + old.Name
}
name := fmt.Sprintf("%d_%s.%s", time.Now().Unix(), action, p.config.TemplateLang)
filePath := filepath.Join(p.config.Dir, name)
return p.app.RunInTransaction(func(txApp core.App) error {
// insert the migration entry
_, err := txApp.DB().Insert(core.DefaultMigrationsTable, dbx.Params{
"file": name,
// use microseconds for more granular applied time in case
// multiple collection changes happens at the ~exact time
"applied": time.Now().UnixMicro(),
}).Execute()
if err != nil {
return err
}
old := oldCollections[e.Model.GetId()]
new, err := p.app.Dao().FindCollectionByNameOrId(e.Model.GetId())
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
// ensure that the local migrations dir exist
if err := os.MkdirAll(p.config.Dir, os.ModePerm); err != nil {
return fmt.Errorf("failed to create migration dir: %w", err)
}
var template string
var templateErr error
if p.config.TemplateLang == TemplateLangJS {
template, templateErr = p.jsDiffTemplate(new, old)
} else {
template, templateErr = p.goDiffTemplate(new, old)
}
if templateErr != nil {
if errors.Is(templateErr, emptyTemplateErr) {
return nil // no changes
}
return fmt.Errorf("failed to resolve template: %w", templateErr)
if err := os.WriteFile(filePath, []byte(template), 0644); err != nil {
return fmt.Errorf("failed to save automigrate file: %w", err)
}
var action string
switch {
case new == nil:
action = "deleted_" + old.Name
case old == nil:
action = "created_" + new.Name
default:
action = "updated_" + old.Name
}
name := fmt.Sprintf("%d_%s.%s", time.Now().Unix(), action, p.config.TemplateLang)
filePath := filepath.Join(p.config.Dir, name)
return p.app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
// insert the migration entry
_, err := txDao.DB().Insert(migrate.DefaultMigrationsTable, dbx.Params{
"file": name,
// use microseconds for more granular applied time in case
// multiple collection changes happens at the ~exact time
"applied": time.Now().UnixMicro(),
}).Execute()
if err != nil {
return err
}
// ensure that the local migrations dir exist
if err := os.MkdirAll(p.config.Dir, os.ModePerm); err != nil {
return fmt.Errorf("failed to create migration dir: %w", err)
}
if err := os.WriteFile(filePath, []byte(template), 0644); err != nil {
return fmt.Errorf("failed to save automigrate file: %w", err)
}
p.updateSingleCachedCollection(new, old)
return nil
})
}
}
func (p *plugin) updateSingleCachedCollection(new, old *models.Collection) {
cached, _ := p.app.Store().Get(collectionsStoreKey).(map[string]*models.Collection)
switch {
case new == nil:
delete(cached, old.Id)
default:
cached[new.Id] = new
}
p.app.Store().Set(collectionsStoreKey, cached)
}
func (p *plugin) refreshCachedCollections() error {
if p.app.Dao() == nil {
return errors.New("app is not initialized yet")
}
var collections []*models.Collection
if err := p.app.Dao().CollectionQuery().All(&collections); err != nil {
return err
}
cached := map[string]*models.Collection{}
for _, c := range collections {
cached[c.Id] = c
}
p.app.Store().Set(collectionsStoreKey, cached)
return nil
}
func (p *plugin) getCachedCollections() (map[string]*models.Collection, error) {
if !p.app.Store().Has(collectionsStoreKey) {
if err := p.refreshCachedCollections(); err != nil {
return nil, err
}
}
result, _ := p.app.Store().Get(collectionsStoreKey).(map[string]*models.Collection)
return result, nil
return nil
})
}
+15 -28
View File
@@ -16,6 +16,7 @@
package migratecmd
import (
"errors"
"fmt"
"os"
"path"
@@ -24,10 +25,7 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tools/inflector"
"github.com/pocketbase/pocketbase/tools/migrate"
"github.com/spf13/cobra"
)
@@ -82,22 +80,9 @@ func Register(app core.App, rootCmd *cobra.Command, config Config) error {
// watch for collection changes
if p.config.Automigrate {
// refresh the cache right after app bootstap
p.app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error {
p.refreshCachedCollections()
return nil
})
// refresh the cache to ensure that it constains the latest changes
// when migrations are applied on server start
p.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
p.refreshCachedCollections()
return nil
})
p.app.OnModelAfterCreate().Add(p.afterCollectionChange())
p.app.OnModelAfterUpdate().Add(p.afterCollectionChange())
p.app.OnModelAfterDelete().Add(p.afterCollectionChange())
p.app.OnCollectionCreateRequest().BindFunc(p.automigrateOnCollectionChange)
p.app.OnCollectionUpdateRequest().BindFunc(p.automigrateOnCollectionChange)
p.app.OnCollectionDeleteRequest().BindFunc(p.automigrateOnCollectionChange)
}
return nil
@@ -139,10 +124,12 @@ func (p *plugin) createCommand() *cobra.Command {
return err
}
default:
runner, err := migrate.NewRunner(p.app.DB(), migrations.AppMigrations)
if err != nil {
return err
}
// note: system migrations are always applied as part of the bootstrap process
var list = core.MigrationsList{}
list.Copy(core.SystemMigrations)
list.Copy(core.AppMigrations)
runner := core.NewMigrationsRunner(p.app, list)
if err := runner.Run(args...); err != nil {
return err
@@ -158,7 +145,7 @@ func (p *plugin) createCommand() *cobra.Command {
func (p *plugin) migrateCreateHandler(template string, args []string, interactive bool) (string, error) {
if len(args) < 1 {
return "", fmt.Errorf("Missing migration file name")
return "", errors.New("Missing migration file name")
}
name := args[0]
@@ -214,9 +201,9 @@ func (p *plugin) migrateCollectionsHandler(args []string, interactive bool) (str
createArgs := []string{"collections_snapshot"}
createArgs = append(createArgs, args...)
collections := []*models.Collection{}
if err := p.app.Dao().CollectionQuery().OrderBy("created ASC").All(&collections); err != nil {
return "", fmt.Errorf("Failed to fetch migrations list: %v", err)
collections := []*core.Collection{}
if err := p.app.CollectionQuery().OrderBy("created ASC").All(&collections); err != nil {
return "", fmt.Errorf("Failed to fetch migrations list: %v\n", err)
}
var template string
@@ -227,7 +214,7 @@ func (p *plugin) migrateCollectionsHandler(args []string, interactive bool) (str
template, templateErr = p.goSnapshotTemplate(collections)
}
if templateErr != nil {
return "", fmt.Errorf("Failed to resolve template: %v", templateErr)
return "", fmt.Errorf("Failed to resolve template: %v\n", templateErr)
}
return p.migrateCreateHandler(template, createArgs, interactive)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff