Skip to content

Commit

Permalink
added plugins subpackage and added basic support for js migrations
Browse files Browse the repository at this point in the history
  • Loading branch information
ganigeorgiev committed Nov 26, 2022
1 parent 3e1a196 commit d8963c6
Show file tree
Hide file tree
Showing 19 changed files with 889 additions and 120 deletions.
6 changes: 3 additions & 3 deletions apis/record_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,10 @@ func autoIgnoreAuthRecordsEmailVisibility(
collection := records[0].Collection()

mappedRecords := make(map[string]*models.Record, len(records))
recordIds := make([]any, 0, len(records))
for _, rec := range records {
recordIds := make([]any, len(records))
for i, rec := range records {
mappedRecords[rec.Id] = rec
recordIds = append(recordIds, rec.Id)
recordIds[i] = rec.Id
}

if requestData != nil && requestData.AuthRecord != nil && mappedRecords[requestData.AuthRecord.Id] != nil {
Expand Down
15 changes: 12 additions & 3 deletions core/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -714,23 +714,32 @@ func (app *BaseApp) createDaoWithHooks(db dbx.Builder) *daos.Dao {
}

dao.AfterCreateFunc = func(eventDao *daos.Dao, m models.Model) {
app.OnModelAfterCreate().Trigger(&ModelEvent{eventDao, m})
err := app.OnModelAfterCreate().Trigger(&ModelEvent{eventDao, m})
if err != nil && app.isDebug {
log.Println(err)
}
}

dao.BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model) error {
return app.OnModelBeforeUpdate().Trigger(&ModelEvent{eventDao, m})
}

dao.AfterUpdateFunc = func(eventDao *daos.Dao, m models.Model) {
app.OnModelAfterUpdate().Trigger(&ModelEvent{eventDao, m})
err := app.OnModelAfterUpdate().Trigger(&ModelEvent{eventDao, m})
if err != nil && app.isDebug {
log.Println(err)
}
}

dao.BeforeDeleteFunc = func(eventDao *daos.Dao, m models.Model) error {
return app.OnModelBeforeDelete().Trigger(&ModelEvent{eventDao, m})
}

dao.AfterDeleteFunc = func(eventDao *daos.Dao, m models.Model) {
app.OnModelAfterDelete().Trigger(&ModelEvent{eventDao, m})
err := app.OnModelAfterDelete().Trigger(&ModelEvent{eventDao, m})
if err != nil && app.isDebug {
log.Println(err)
}
}

return dao
Expand Down
4 changes: 2 additions & 2 deletions daos/record_expand.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch
}

recordIds := make([]any, len(records))
for _, record := range records {
recordIds = append(recordIds, record.Id)
for i, record := range records {
recordIds[i] = record.Id
}

indirectRecords, err := dao.FindRecordsByExpr(
Expand Down
41 changes: 14 additions & 27 deletions examples/base/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,29 @@ package main

import (
"log"
"os"
"path/filepath"
"strings"

"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/plugins/jsvm"
"github.com/pocketbase/pocketbase/plugins/migratecmd"
"github.com/pocketbase/pocketbase/plugins/publicdir"
)

func defaultPublicDir() string {
if strings.HasPrefix(os.Args[0], os.TempDir()) {
// most likely ran with go run
return "./pb_public"
}

return filepath.Join(os.Args[0], "../pb_public")
}

func main() {
app := pocketbase.New()

var publicDirFlag string

// add "--publicDir" option flag
app.RootCmd.PersistentFlags().StringVar(
&publicDirFlag,
"publicDir",
defaultPublicDir(),
"the directory to serve static files",
)
// load js pb_migrations
jsvm.MustRegisterMigrationsLoader(app, nil)

app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// serves static files from the provided public dir (if exists)
e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS(publicDirFlag), true))
// migrate command (with js templates)
migratecmd.MustRegister(app, app.RootCmd, &migratecmd.Options{
TemplateLang: migratecmd.TemplateLangJS,
AutoMigrate: true,
})

return nil
// pb_public dir
publicdir.MustRegister(app, &publicdir.Options{
FlagsCmd: app.RootCmd,
IndexFallback: true,
})

if err := app.Start(); err != nil {
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ require (
github.com/aws/aws-sdk-go v1.44.141
github.com/disintegration/imaging v1.6.2
github.com/domodwyer/mailyak/v3 v3.3.4
github.com/dop251/goja v0.0.0-20221118162653-d4bf6fde1b86
github.com/dop251/goja_nodejs v0.0.0-20221009164102-3aa5028e57f6
github.com/fatih/color v1.13.0
github.com/gabriel-vasile/mimetype v1.4.1
github.com/ganigeorgiev/fexpr v0.1.1
Expand Down Expand Up @@ -45,6 +47,8 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.17.4 // indirect
github.com/aws/smithy-go v1.13.4 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 // indirect
Expand Down
65 changes: 15 additions & 50 deletions go.sum

Large diffs are not rendered by default.

110 changes: 110 additions & 0 deletions plugins/jsvm/migrations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package jsvm

import (
"os"
"path/filepath"

"github.com/dop251/goja_nodejs/console"
"github.com/dop251/goja_nodejs/require"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)

// MigrationsLoaderOptions defines optional struct to customize the default plugin behavior.
type MigrationsLoaderOptions struct {
// Dir is the app migrations directory from where the js files will be loaded
// (default to pb_data/migrations)
Dir string
}

// migrationsLoader is the plugin definition.
// Usually it is instantiated via RegisterMigrationsLoader or MustRegisterMigrationsLoader.
type migrationsLoader struct {
app core.App
options *MigrationsLoaderOptions
}

//
// MustRegisterMigrationsLoader registers the plugin to the provided
// app instance and panics if it fails.
//
// It it calls RegisterMigrationsLoader(app, options)
//
// If options is nil, by default the js files from pb_data/migrations are loaded.
// Set custom options.Dir if you want to change it to some other directory.
func MustRegisterMigrationsLoader(app core.App, options *MigrationsLoaderOptions) {
if err := RegisterMigrationsLoader(app, options); err != nil {
panic(err)
}
}

// RegisterMigrationsLoader registers the plugin to the provided app instance.
//
// If options is nil, by default the js files from pb_data/migrations are loaded.
// Set custom options.Dir if you want to change it to some other directory.
func RegisterMigrationsLoader(app core.App, options *MigrationsLoaderOptions) error {
l := &migrationsLoader{app: app}

if options != nil {
l.options = options
} else {
l.options = &MigrationsLoaderOptions{}
}

if l.options.Dir == "" {
l.options.Dir = filepath.Join(app.DataDir(), "../pb_migrations")
}

files, err := readDirFiles(l.options.Dir)
if err != nil {
return err
}

registry := new(require.Registry) // this can be shared by multiple runtimes

for file, content := range files {
vm := NewBaseVM(l.app)
registry.Enable(vm)
console.Enable(vm)

vm.Set("migrate", func(up, down func(db dbx.Builder) error) {
m.AppMigrations.Register(up, down, file)
})

_, err := vm.RunString(string(content))
if err != nil {
return err
}
}

return nil
}

// readDirFiles returns a map with all directory files and their content.
//
// If directory with dirPath is missing, it returns an empty map and no error.
func readDirFiles(dirPath string) (map[string][]byte, error) {
files, err := os.ReadDir(dirPath)
if err != nil {
if os.IsNotExist(err) {
return map[string][]byte{}, nil
}
return nil, err
}

result := map[string][]byte{}

for _, f := range files {
if f.IsDir() {
continue
}
raw, err := os.ReadFile(filepath.Join(dirPath, f.Name()))
if err != nil {
return nil, err
}
result[f.Name()] = raw
}

return result, nil
}
135 changes: 135 additions & 0 deletions plugins/jsvm/vm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package jsvm

import (
"encoding/json"

"github.com/dop251/goja"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
)

func NewBaseVM(app core.App) *goja.Runtime {
vm := goja.New()
vm.SetFieldNameMapper(goja.UncapFieldNameMapper())

vm.Set("$app", app)

vm.Set("unmarshal", func(src map[string]any, dest any) (any, error) {
raw, err := json.Marshal(src)
if err != nil {
return nil, err
}

if err := json.Unmarshal(raw, &dest); err != nil {
return nil, err
}

return dest, nil
})

collectionConstructor(vm)
recordConstructor(vm)
adminConstructor(vm)
daoConstructor(vm)
dbxBinds(vm)

return vm
}

func collectionConstructor(vm *goja.Runtime) {
vm.Set("Collection", func(call goja.ConstructorCall) *goja.Object {
instance := &models.Collection{}
instanceValue := vm.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
})
}

func recordConstructor(vm *goja.Runtime) {
vm.Set("Record", func(call goja.ConstructorCall) *goja.Object {
instance := &models.Record{}
instanceValue := vm.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
})
}

func adminConstructor(vm *goja.Runtime) {
vm.Set("Admin", func(call goja.ConstructorCall) *goja.Object {
instance := &models.Admin{}
instanceValue := vm.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
})
}

func daoConstructor(vm *goja.Runtime) {
vm.Set("Dao", func(call goja.ConstructorCall) *goja.Object {
db, ok := call.Argument(0).Export().(dbx.Builder)
if !ok || db == nil {
panic("missing required Dao(db) argument")
}

instance := daos.New(db)
instanceValue := vm.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
})
}

func dbxBinds(vm *goja.Runtime) {
obj := vm.NewObject()
vm.Set("$dbx", obj)

obj.Set("exp", dbx.NewExp)
obj.Set("hashExp", func(data map[string]any) dbx.HashExp {
exp := dbx.HashExp{}
for k, v := range data {
exp[k] = v
}
return exp
})
obj.Set("not", dbx.Not)
obj.Set("and", dbx.And)
obj.Set("or", dbx.Or)
obj.Set("in", dbx.In)
obj.Set("notIn", dbx.NotIn)
obj.Set("like", dbx.Like)
obj.Set("orLike", dbx.OrLike)
obj.Set("notLike", dbx.NotLike)
obj.Set("orNotLike", dbx.OrNotLike)
obj.Set("exists", dbx.Exists)
obj.Set("notExists", dbx.NotExists)
obj.Set("between", dbx.Between)
obj.Set("notBetween", dbx.NotBetween)
}

func apisBind(vm *goja.Runtime) {
obj := vm.NewObject()
vm.Set("$apis", obj)

// middlewares
obj.Set("requireRecordAuth", apis.RequireRecordAuth)
obj.Set("requireRecordAuth", apis.RequireRecordAuth)
obj.Set("requireSameContextRecordAuth", apis.RequireSameContextRecordAuth)
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)

// api errors
obj.Set("notFoundError", apis.NewNotFoundError)
obj.Set("badRequestError", apis.NewBadRequestError)
obj.Set("forbiddenError", apis.NewForbiddenError)
obj.Set("unauthorizedError", apis.NewUnauthorizedError)

// record helpers
obj.Set("getRequestData", apis.GetRequestData)
obj.Set("requestData", apis.RequestData)
obj.Set("enrichRecord", apis.EnrichRecord)
obj.Set("enrichRecords", apis.EnrichRecords)
}
Loading

0 comments on commit d8963c6

Please sign in to comment.