merged jsvm migrations and hooks and updated the ambient TS types location
This commit is contained in:
parent
1571ebe4eb
commit
a672ab959f
|
@ -48,7 +48,7 @@
|
||||||
new: apis.Serve(app core.App, config apis.ServeConfig)
|
new: apis.Serve(app core.App, config apis.ServeConfig)
|
||||||
|
|
||||||
old: jsvm.MustRegisterMigrations(app core.App, options *jsvm.MigrationsOptions)
|
old: jsvm.MustRegisterMigrations(app core.App, options *jsvm.MigrationsOptions)
|
||||||
new: jsvm.MustRegisterMigrations(app core.App, config jsvm.MigrationsConfig)
|
new: jsvm.MustRegister(app core.App, config jsvm.Config)
|
||||||
|
|
||||||
old: ghupdate.MustRegister(app core.App, rootCmd *cobra.Command, options *ghupdate.Options)
|
old: ghupdate.MustRegister(app core.App, rootCmd *cobra.Command, options *ghupdate.Options)
|
||||||
new: ghupdate.MustRegister(app core.App, rootCmd *cobra.Command, config ghupdate.Config)
|
new: ghupdate.MustRegister(app core.App, rootCmd *cobra.Command, config ghupdate.Config)
|
||||||
|
@ -59,15 +59,18 @@
|
||||||
|
|
||||||
- (@todo docs) Added new experimental JavaScript app hooks binding via [goja](https://github.com/dop251/goja).
|
- (@todo docs) Added new experimental JavaScript app hooks binding via [goja](https://github.com/dop251/goja).
|
||||||
They are available by default with the prebuilt executable if you add a `*.pb.js` file in `pb_hooks` directory.
|
They are available by default with the prebuilt executable if you add a `*.pb.js` file in `pb_hooks` directory.
|
||||||
To enable them as part of a custom Go build:
|
To enable them as part of a custom Go build, you need to register the `jsvm` plugin:
|
||||||
```go
|
```go
|
||||||
jsvm.MustRegisterHooks(app core.App, config jsvm.HooksConfig{})
|
jsvm.MustRegister(app core.App, config jsvm.Config{})
|
||||||
```
|
```
|
||||||
|
(@todo add note about autogenerated hooks and migrations types...)
|
||||||
|
|
||||||
- Refactored `apis.ApiError` validation errors serialization to allow `map[string]error` and `map[string]any` when generating the public safe formatted `ApiError.Data`.
|
- Refactored `apis.ApiError` validation errors serialization to allow `map[string]error` and `map[string]any` when generating the public safe formatted `ApiError.Data`.
|
||||||
|
|
||||||
- Added `types.JsonMap.Get(k)` and `types.JsonMap.Set(k, v)` helpers for the cases where the type aliased direct map access is not allowed (eg. in [goja](https://pkg.go.dev/github.com/dop251/goja#hdr-Maps_with_methods)).
|
- Added `types.JsonMap.Get(k)` and `types.JsonMap.Set(k, v)` helpers for the cases where the type aliased direct map access is not allowed (eg. in [goja](https://pkg.go.dev/github.com/dop251/goja#hdr-Maps_with_methods)).
|
||||||
|
|
||||||
|
- Fixed `migrate down` not returning the correct `lastAppliedMigrations()` when the stored migration applied time is in seconds.
|
||||||
|
|
||||||
|
|
||||||
## v0.16.6
|
## v0.16.6
|
||||||
|
|
||||||
|
|
|
@ -84,15 +84,11 @@ func main() {
|
||||||
// Plugins and hooks:
|
// Plugins and hooks:
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
// load js pb_hooks
|
// load jsvm (hooks and migrations)
|
||||||
jsvm.MustRegisterHooks(app, jsvm.HooksConfig{
|
jsvm.MustRegister(app, jsvm.Config{
|
||||||
Dir: hooksDir,
|
MigrationsDir: migrationsDir,
|
||||||
Watch: hooksWatch,
|
HooksDir: hooksDir,
|
||||||
})
|
HooksWatch: hooksWatch,
|
||||||
|
|
||||||
// load js pb_migrations
|
|
||||||
jsvm.MustRegisterMigrations(app, jsvm.MigrationsConfig{
|
|
||||||
Dir: migrationsDir,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// migrate command (with js templates)
|
// migrate command (with js templates)
|
||||||
|
|
|
@ -18,10 +18,7 @@ package jsvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||||
|
@ -360,47 +357,6 @@ func structConstructorUnmarshal(vm *goja.Runtime, call goja.ConstructorCall, ins
|
||||||
return instanceValue
|
return instanceValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// filesContent returns a map with all direct files within the specified dir and their content.
|
|
||||||
//
|
|
||||||
// If directory with dirPath is missing or no files matching the pattern were found,
|
|
||||||
// it returns an empty map and no error.
|
|
||||||
//
|
|
||||||
// If pattern is empty string it matches all root files.
|
|
||||||
func filesContent(dirPath string, pattern 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
|
|
||||||
}
|
|
||||||
|
|
||||||
var exp *regexp.Regexp
|
|
||||||
if pattern != "" {
|
|
||||||
var err error
|
|
||||||
if exp, err = regexp.Compile(pattern); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result := map[string][]byte{}
|
|
||||||
|
|
||||||
for _, f := range files {
|
|
||||||
if f.IsDir() || (exp != nil && !exp.MatchString(f.Name())) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := os.ReadFile(filepath.Join(dirPath, f.Name()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result[f.Name()] = raw
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// newDynamicModel creates a new dynamic struct with fields based
|
// newDynamicModel creates a new dynamic struct with fields based
|
||||||
// on the specified "shape".
|
// on the specified "shape".
|
||||||
//
|
//
|
|
@ -1,233 +0,0 @@
|
||||||
package jsvm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"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"
|
|
||||||
"github.com/fsnotify/fsnotify"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/plugins/jsvm/internal/docs/generated"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
hooksExtension = ".pb.js"
|
|
||||||
|
|
||||||
typesFileName = ".types.d.ts"
|
|
||||||
|
|
||||||
typesReferenceDirective = `/// <reference path="./` + typesFileName + `" />`
|
|
||||||
)
|
|
||||||
|
|
||||||
// HooksConfig defines the config options of the JS app hooks plugin.
|
|
||||||
type HooksConfig struct {
|
|
||||||
// Dir specifies the directory with the JS app hooks.
|
|
||||||
//
|
|
||||||
// If not set it fallbacks to a relative "pb_data/../pb_hooks" directory.
|
|
||||||
Dir string
|
|
||||||
|
|
||||||
// Watch enables auto app restarts when a JS app hook file changes.
|
|
||||||
//
|
|
||||||
// Note that currently the application cannot be automatically restarted on Windows
|
|
||||||
// because the restart process relies on execve.
|
|
||||||
Watch bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustRegisterHooks registers the JS hooks plugin to
|
|
||||||
// the provided app instance and panics if it fails.
|
|
||||||
//
|
|
||||||
// Example usage:
|
|
||||||
//
|
|
||||||
// jsvm.MustRegisterHooks(app, jsvm.HooksConfig{})
|
|
||||||
func MustRegisterHooks(app core.App, config HooksConfig) {
|
|
||||||
if err := RegisterHooks(app, config); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterHooks registers the JS hooks plugin to the provided app instance.
|
|
||||||
func RegisterHooks(app core.App, config HooksConfig) error {
|
|
||||||
p := &hooks{app: app, config: config}
|
|
||||||
|
|
||||||
if p.config.Dir == "" {
|
|
||||||
p.config.Dir = filepath.Join(app.DataDir(), "../pb_hooks")
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch all js hooks sorted by their filename
|
|
||||||
files, err := filesContent(p.config.Dir, `^.*`+regexp.QuoteMeta(hooksExtension)+`$`)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepend the types reference directive to empty files
|
|
||||||
for name, content := range files {
|
|
||||||
if len(content) != 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
path := filepath.Join(p.config.Dir, name)
|
|
||||||
if err := prependToEmptyFile(path, typesReferenceDirective+"\n\n"); err != nil {
|
|
||||||
color.Yellow("Unable to prepend the types reference: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registry := new(require.Registry) // this can be shared by multiple runtimes
|
|
||||||
|
|
||||||
loop := eventloop.NewEventLoop()
|
|
||||||
|
|
||||||
loop.Run(func(vm *goja.Runtime) {
|
|
||||||
registry.Enable(vm)
|
|
||||||
console.Enable(vm)
|
|
||||||
process.Enable(vm)
|
|
||||||
baseBinds(vm)
|
|
||||||
dbxBinds(vm)
|
|
||||||
filesystemBinds(vm)
|
|
||||||
tokensBinds(vm)
|
|
||||||
securityBinds(vm)
|
|
||||||
formsBinds(vm)
|
|
||||||
apisBinds(vm)
|
|
||||||
|
|
||||||
vm.Set("$app", app)
|
|
||||||
|
|
||||||
for file, content := range files {
|
|
||||||
_, err := vm.RunString(string(content))
|
|
||||||
if err != nil {
|
|
||||||
if p.config.Watch {
|
|
||||||
color.Red("Failed to execute %s: %v", file, err)
|
|
||||||
} else {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
loop.Start()
|
|
||||||
|
|
||||||
app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error {
|
|
||||||
// always update the app types on start to ensure that
|
|
||||||
// the user has the latest generated declarations
|
|
||||||
if len(files) > 0 {
|
|
||||||
if err := p.saveTypesFile(); err != nil {
|
|
||||||
color.Yellow("Unable to save app types file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
app.OnTerminate().Add(func(e *core.TerminateEvent) error {
|
|
||||||
loop.StopNoWait()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if p.config.Watch {
|
|
||||||
return p.watchFiles()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type hooks struct {
|
|
||||||
app core.App
|
|
||||||
config HooksConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *hooks) watchFiles() error {
|
|
||||||
watcher, err := fsnotify.NewWatcher()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var debounceTimer *time.Timer
|
|
||||||
|
|
||||||
stopDebounceTimer := func() {
|
|
||||||
if debounceTimer != nil {
|
|
||||||
debounceTimer.Stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p.app.OnTerminate().Add(func(e *core.TerminateEvent) error {
|
|
||||||
watcher.Close()
|
|
||||||
|
|
||||||
stopDebounceTimer()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
// start listening for events.
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event, ok := <-watcher.Events:
|
|
||||||
if !ok {
|
|
||||||
stopDebounceTimer()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip TS declaration files change
|
|
||||||
if strings.HasSuffix(event.Name, ".d.ts") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
stopDebounceTimer()
|
|
||||||
debounceTimer = time.AfterFunc(50*time.Millisecond, func() {
|
|
||||||
// app restart is currently not supported on Windows
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
color.Yellow("File %s changed, please restart the app", event.Name)
|
|
||||||
} else {
|
|
||||||
color.Yellow("File %s changed, restarting...", event.Name)
|
|
||||||
if err := p.app.Restart(); err != nil {
|
|
||||||
color.Red("Failed to restart the app:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
case err, ok := <-watcher.Errors:
|
|
||||||
if !ok {
|
|
||||||
stopDebounceTimer()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
color.Red("Watch error:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// add the directory to watch
|
|
||||||
err = watcher.Add(p.config.Dir)
|
|
||||||
if err != nil {
|
|
||||||
watcher.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *hooks) saveTypesFile() error {
|
|
||||||
data, _ := generated.Types.ReadFile("types.d.ts")
|
|
||||||
|
|
||||||
if err := os.WriteFile(filepath.Join(p.config.Dir, typesFileName), data, 0644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// prependToEmptyFile prepends the specified text to an empty file.
|
|
||||||
//
|
|
||||||
// If the file is not empty this method does nothing.
|
|
||||||
func prependToEmptyFile(path, text string) error {
|
|
||||||
info, err := os.Stat(path)
|
|
||||||
|
|
||||||
if err == nil && info.Size() == 0 {
|
|
||||||
return os.WriteFile(path, []byte(text), 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -309,6 +309,20 @@ declare namespace $apis {
|
||||||
let enrichRecord: apis.enrichRecord
|
let enrichRecord: apis.enrichRecord
|
||||||
let enrichRecords: apis.enrichRecords
|
let enrichRecords: apis.enrichRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// migrate only
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate defines a single migration upgrade/downgrade action.
|
||||||
|
*
|
||||||
|
* Note that this method is available only in pb_migrations context.
|
||||||
|
*/
|
||||||
|
declare function migrate(
|
||||||
|
up: (db: dbx.Builder) => void,
|
||||||
|
down?: (db: dbx.Builder) => void
|
||||||
|
): void;
|
||||||
`
|
`
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,376 @@
|
||||||
|
package jsvm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
"github.com/pocketbase/pocketbase/plugins/jsvm/internal/docs/generated"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hooksExtension = ".pb.js"
|
||||||
|
migrationsExtension = ".js"
|
||||||
|
|
||||||
|
typesFileName = "types.d.ts"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config defines the config options of the jsvm plugin.
|
||||||
|
type Config struct {
|
||||||
|
// MigrationsDir specifies the JS migrations directory.
|
||||||
|
//
|
||||||
|
// If not set it fallbacks to a relative "pb_data/../pb_migrations" directory.
|
||||||
|
MigrationsDir string
|
||||||
|
|
||||||
|
// HooksDir specifies the JS app hooks directory.
|
||||||
|
//
|
||||||
|
// If not set it fallbacks to a relative "pb_data/../pb_hooks" directory.
|
||||||
|
HooksDir string
|
||||||
|
|
||||||
|
// HooksWatch enables auto app restarts when a JS app hook file changes.
|
||||||
|
//
|
||||||
|
// Note that currently the application cannot be automatically restarted on Windows
|
||||||
|
// because the restart process relies on execve.
|
||||||
|
HooksWatch bool
|
||||||
|
|
||||||
|
// TypesDir specifies the directory where to store the embedded
|
||||||
|
// TypeScript declarations file.
|
||||||
|
//
|
||||||
|
// If not set it fallbacks to "pb_data".
|
||||||
|
TypesDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustRegister registers the jsvm plugin in the provided app instance
|
||||||
|
// and panics if it fails.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// jsvm.MustRegister(app, jsvm.Config{})
|
||||||
|
func MustRegister(app core.App, config Config) {
|
||||||
|
if err := Register(app, config); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register registers the jsvm plugin in the provided app instance.
|
||||||
|
func Register(app core.App, config Config) error {
|
||||||
|
p := &plugin{app: app, config: config}
|
||||||
|
|
||||||
|
if p.config.HooksDir == "" {
|
||||||
|
p.config.HooksDir = filepath.Join(app.DataDir(), "../pb_hooks")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.config.MigrationsDir == "" {
|
||||||
|
p.config.MigrationsDir = filepath.Join(app.DataDir(), "../pb_migrations")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.config.TypesDir == "" {
|
||||||
|
p.config.TypesDir = app.DataDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
p.app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error {
|
||||||
|
// always update the app types on start to ensure that
|
||||||
|
// the user has the latest generated declarations
|
||||||
|
if err := p.saveTypesFile(); err != nil {
|
||||||
|
color.Yellow("Unable to save app types file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := p.registerMigrations(); err != nil {
|
||||||
|
return fmt.Errorf("registerHooks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.registerHooks(); err != nil {
|
||||||
|
return fmt.Errorf("registerHooks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type plugin struct {
|
||||||
|
app core.App
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerMigrations registers the JS migrations loader.
|
||||||
|
func (p *plugin) registerMigrations() error {
|
||||||
|
// fetch all js migrations sorted by their filename
|
||||||
|
files, err := filesContent(p.config.MigrationsDir, `^.*`+regexp.QuoteMeta(migrationsExtension)+`$`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
registry := new(require.Registry) // this can be shared by multiple runtimes
|
||||||
|
|
||||||
|
for file, content := range files {
|
||||||
|
vm := goja.New()
|
||||||
|
registry.Enable(vm)
|
||||||
|
console.Enable(vm)
|
||||||
|
process.Enable(vm)
|
||||||
|
baseBinds(vm)
|
||||||
|
dbxBinds(vm)
|
||||||
|
filesystemBinds(vm)
|
||||||
|
tokensBinds(vm)
|
||||||
|
securityBinds(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 fmt.Errorf("failed to run migration %s: %w", file, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerHooks registers the JS app hooks loader.
|
||||||
|
func (p *plugin) registerHooks() error {
|
||||||
|
// fetch all js hooks sorted by their filename
|
||||||
|
files, err := filesContent(p.config.HooksDir, `^.*`+regexp.QuoteMeta(hooksExtension)+`$`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepend the types reference directive to empty files
|
||||||
|
//
|
||||||
|
// note: it is loaded during startup to handle conveniently also
|
||||||
|
// the case when the HooksWatch option is enabled and the application
|
||||||
|
// restart on newly created file
|
||||||
|
for name, content := range files {
|
||||||
|
if len(content) != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
path := filepath.Join(p.config.HooksDir, name)
|
||||||
|
directive := `/// <reference path="` + p.relativeTypesPath(p.config.HooksDir) + `" />`
|
||||||
|
if err := prependToEmptyFile(path, directive+"\n\n"); err != nil {
|
||||||
|
color.Yellow("Unable to prepend the types reference: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry := new(require.Registry) // this can be shared by multiple runtimes
|
||||||
|
|
||||||
|
loop := eventloop.NewEventLoop()
|
||||||
|
|
||||||
|
loop.Run(func(vm *goja.Runtime) {
|
||||||
|
registry.Enable(vm)
|
||||||
|
console.Enable(vm)
|
||||||
|
process.Enable(vm)
|
||||||
|
baseBinds(vm)
|
||||||
|
dbxBinds(vm)
|
||||||
|
filesystemBinds(vm)
|
||||||
|
tokensBinds(vm)
|
||||||
|
securityBinds(vm)
|
||||||
|
formsBinds(vm)
|
||||||
|
apisBinds(vm)
|
||||||
|
|
||||||
|
vm.Set("$app", p.app)
|
||||||
|
|
||||||
|
for file, content := range files {
|
||||||
|
_, err := vm.RunString(string(content))
|
||||||
|
if err != nil {
|
||||||
|
if p.config.HooksWatch {
|
||||||
|
color.Red("Failed to execute %s: %v", file, err)
|
||||||
|
} else {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
loop.Start()
|
||||||
|
|
||||||
|
p.app.OnTerminate().Add(func(e *core.TerminateEvent) error {
|
||||||
|
loop.StopNoWait()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if p.config.HooksWatch {
|
||||||
|
return p.watchHooks()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchHooks initializes a hooks file watcher that will restart the
|
||||||
|
// application (*if possible) in case of a change in the hooks directory.
|
||||||
|
func (p *plugin) watchHooks() error {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var debounceTimer *time.Timer
|
||||||
|
|
||||||
|
stopDebounceTimer := func() {
|
||||||
|
if debounceTimer != nil {
|
||||||
|
debounceTimer.Stop()
|
||||||
|
debounceTimer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.app.OnTerminate().Add(func(e *core.TerminateEvent) error {
|
||||||
|
watcher.Close()
|
||||||
|
|
||||||
|
stopDebounceTimer()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// start listening for events.
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
stopDebounceTimer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stopDebounceTimer()
|
||||||
|
debounceTimer = time.AfterFunc(50*time.Millisecond, func() {
|
||||||
|
// app restart is currently not supported on Windows
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
color.Yellow("File %s changed, please restart the app", event.Name)
|
||||||
|
} else {
|
||||||
|
color.Yellow("File %s changed, restarting...", event.Name)
|
||||||
|
if err := p.app.Restart(); err != nil {
|
||||||
|
color.Red("Failed to restart the app:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
case err, ok := <-watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
stopDebounceTimer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
color.Red("Watch error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// add the directory to watch
|
||||||
|
err = watcher.Add(p.config.HooksDir)
|
||||||
|
if err != nil {
|
||||||
|
watcher.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fullTypesPathReturns returns the full path to the generated TS file.
|
||||||
|
func (p *plugin) fullTypesPath() string {
|
||||||
|
return filepath.Join(p.config.TypesDir, typesFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// relativeTypesPath returns a path to the generated TS file relative
|
||||||
|
// to the specified basepath.
|
||||||
|
//
|
||||||
|
// It fallbacks to the full path if generating the relative path fails.
|
||||||
|
func (p *plugin) relativeTypesPath(basepath string) string {
|
||||||
|
fullPath := p.fullTypesPath()
|
||||||
|
|
||||||
|
rel, err := filepath.Rel(basepath, fullPath)
|
||||||
|
if err != nil {
|
||||||
|
// fallback to the full path
|
||||||
|
rel = fullPath
|
||||||
|
}
|
||||||
|
|
||||||
|
return rel
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveTypesFile saves the embeded TS declarations as a file on the disk.
|
||||||
|
func (p *plugin) saveTypesFile() error {
|
||||||
|
fullPath := p.fullTypesPath()
|
||||||
|
|
||||||
|
// ensure that the types directory exists
|
||||||
|
dir := filepath.Dir(fullPath)
|
||||||
|
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrieve the types data to write
|
||||||
|
data, err := generated.Types.ReadFile(typesFileName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(fullPath, data, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prependToEmptyFile prepends the specified text to an empty file.
|
||||||
|
//
|
||||||
|
// If the file is not empty this method does nothing.
|
||||||
|
func prependToEmptyFile(path, text string) error {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
|
||||||
|
if err == nil && info.Size() == 0 {
|
||||||
|
return os.WriteFile(path, []byte(text), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// filesContent returns a map with all direct files within the specified dir and their content.
|
||||||
|
//
|
||||||
|
// If directory with dirPath is missing or no files matching the pattern were found,
|
||||||
|
// it returns an empty map and no error.
|
||||||
|
//
|
||||||
|
// If pattern is empty string it matches all root files.
|
||||||
|
func filesContent(dirPath string, pattern 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
|
||||||
|
}
|
||||||
|
|
||||||
|
var exp *regexp.Regexp
|
||||||
|
if pattern != "" {
|
||||||
|
var err error
|
||||||
|
if exp, err = regexp.Compile(pattern); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := map[string][]byte{}
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
if f.IsDir() || (exp != nil && !exp.MatchString(f.Name())) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := os.ReadFile(filepath.Join(dirPath, f.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result[f.Name()] = raw
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
|
@ -1,78 +0,0 @@
|
||||||
package jsvm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
"github.com/dop251/goja_nodejs/console"
|
|
||||||
"github.com/dop251/goja_nodejs/process"
|
|
||||||
"github.com/dop251/goja_nodejs/require"
|
|
||||||
"github.com/pocketbase/dbx"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
m "github.com/pocketbase/pocketbase/migrations"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MigrationsConfig defines the config options of the JS migrations loader plugin.
|
|
||||||
type MigrationsConfig struct {
|
|
||||||
// Dir specifies the directory with the JS migrations.
|
|
||||||
//
|
|
||||||
// If not set it fallbacks to a relative "pb_data/../pb_migrations" directory.
|
|
||||||
Dir string
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustRegisterMigrations registers the JS migrations loader plugin to
|
|
||||||
// the provided app instance and panics if it fails.
|
|
||||||
//
|
|
||||||
// Example usage:
|
|
||||||
//
|
|
||||||
// jsvm.MustRegisterMigrations(app, jsvm.MigrationsConfig{})
|
|
||||||
func MustRegisterMigrations(app core.App, config MigrationsConfig) {
|
|
||||||
if err := RegisterMigrations(app, config); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterMigrations registers the JS migrations loader hooks plugin
|
|
||||||
// to the provided app instance.
|
|
||||||
func RegisterMigrations(app core.App, config MigrationsConfig) error {
|
|
||||||
l := &migrations{app: app, config: config}
|
|
||||||
|
|
||||||
if l.config.Dir == "" {
|
|
||||||
l.config.Dir = filepath.Join(app.DataDir(), "../pb_migrations")
|
|
||||||
}
|
|
||||||
|
|
||||||
files, err := filesContent(l.config.Dir, `^.*\.js$`)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
registry := new(require.Registry) // this can be shared by multiple runtimes
|
|
||||||
|
|
||||||
for file, content := range files {
|
|
||||||
vm := goja.New()
|
|
||||||
registry.Enable(vm)
|
|
||||||
console.Enable(vm)
|
|
||||||
process.Enable(vm)
|
|
||||||
baseBinds(vm)
|
|
||||||
dbxBinds(vm)
|
|
||||||
tokensBinds(vm)
|
|
||||||
securityBinds(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 fmt.Errorf("failed to run migration %s: %w", file, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type migrations struct {
|
|
||||||
app core.App
|
|
||||||
config MigrationsConfig
|
|
||||||
}
|
|
|
@ -22,6 +22,7 @@ func TestAutomigrateCollectionCreate(t *testing.T) {
|
||||||
{
|
{
|
||||||
migratecmd.TemplateLangJS,
|
migratecmd.TemplateLangJS,
|
||||||
`
|
`
|
||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
migrate((db) => {
|
migrate((db) => {
|
||||||
const collection = new Collection({
|
const collection = new Collection({
|
||||||
"id": "new_id",
|
"id": "new_id",
|
||||||
|
@ -194,6 +195,7 @@ func TestAutomigrateCollectionDelete(t *testing.T) {
|
||||||
{
|
{
|
||||||
migratecmd.TemplateLangJS,
|
migratecmd.TemplateLangJS,
|
||||||
`
|
`
|
||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
migrate((db) => {
|
migrate((db) => {
|
||||||
const dao = new Dao(db);
|
const dao = new Dao(db);
|
||||||
const collection = dao.findCollectionByNameOrId("test123");
|
const collection = dao.findCollectionByNameOrId("test123");
|
||||||
|
@ -372,6 +374,7 @@ func TestAutomigrateCollectionUpdate(t *testing.T) {
|
||||||
{
|
{
|
||||||
migratecmd.TemplateLangJS,
|
migratecmd.TemplateLangJS,
|
||||||
`
|
`
|
||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
migrate((db) => {
|
migrate((db) => {
|
||||||
const dao = new Dao(db)
|
const dao = new Dao(db)
|
||||||
const collection = dao.findCollectionByNameOrId("test123")
|
const collection = dao.findCollectionByNameOrId("test123")
|
||||||
|
|
|
@ -15,6 +15,11 @@ import (
|
||||||
const (
|
const (
|
||||||
TemplateLangJS = "js"
|
TemplateLangJS = "js"
|
||||||
TemplateLangGo = "go"
|
TemplateLangGo = "go"
|
||||||
|
|
||||||
|
// note: this usually should be configurable similar to the jsvm plugin,
|
||||||
|
// but for simplicity is static as users can easily change the
|
||||||
|
// reference path if they use custom dirs structure
|
||||||
|
jsTypesDirective = `/// <reference path="../pb_data/types.d.ts" />` + "\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
var emptyTemplateErr = errors.New("empty template")
|
var emptyTemplateErr = errors.New("empty template")
|
||||||
|
@ -24,7 +29,7 @@ var emptyTemplateErr = errors.New("empty template")
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
func (p *plugin) jsBlankTemplate() (string, error) {
|
func (p *plugin) jsBlankTemplate() (string, error) {
|
||||||
const template = `migrate((db) => {
|
const template = jsTypesDirective + `migrate((db) => {
|
||||||
// add up queries...
|
// add up queries...
|
||||||
}, (db) => {
|
}, (db) => {
|
||||||
// add down queries...
|
// add down queries...
|
||||||
|
@ -40,7 +45,7 @@ func (p *plugin) jsSnapshotTemplate(collections []*models.Collection) (string, e
|
||||||
return "", fmt.Errorf("failed to serialize collections list: %w", err)
|
return "", fmt.Errorf("failed to serialize collections list: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = `migrate((db) => {
|
const template = jsTypesDirective + `migrate((db) => {
|
||||||
const snapshot = %s;
|
const snapshot = %s;
|
||||||
|
|
||||||
const collections = snapshot.map((item) => new Collection(item));
|
const collections = snapshot.map((item) => new Collection(item));
|
||||||
|
@ -60,7 +65,7 @@ func (p *plugin) jsCreateTemplate(collection *models.Collection) (string, error)
|
||||||
return "", fmt.Errorf("failed to serialize collections list: %w", err)
|
return "", fmt.Errorf("failed to serialize collections list: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = `migrate((db) => {
|
const template = jsTypesDirective + `migrate((db) => {
|
||||||
const collection = new Collection(%s);
|
const collection = new Collection(%s);
|
||||||
|
|
||||||
return Dao(db).saveCollection(collection);
|
return Dao(db).saveCollection(collection);
|
||||||
|
@ -81,7 +86,7 @@ func (p *plugin) jsDeleteTemplate(collection *models.Collection) (string, error)
|
||||||
return "", fmt.Errorf("failed to serialize collections list: %w", err)
|
return "", fmt.Errorf("failed to serialize collections list: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = `migrate((db) => {
|
const template = jsTypesDirective + `migrate((db) => {
|
||||||
const dao = new Dao(db);
|
const dao = new Dao(db);
|
||||||
const collection = dao.findCollectionByNameOrId(%q);
|
const collection = dao.findCollectionByNameOrId(%q);
|
||||||
|
|
||||||
|
@ -294,7 +299,7 @@ func (p *plugin) jsDiffTemplate(new *models.Collection, old *models.Collection)
|
||||||
up := strings.Join(upParts, "\n ")
|
up := strings.Join(upParts, "\n ")
|
||||||
down := strings.Join(downParts, "\n ")
|
down := strings.Join(downParts, "\n ")
|
||||||
|
|
||||||
const template = `migrate((db) => {
|
const template = jsTypesDirective + `migrate((db) => {
|
||||||
const dao = new Dao(db)
|
const dao = new Dao(db)
|
||||||
const collection = dao.findCollectionByNameOrId(%q)
|
const collection = dao.findCollectionByNameOrId(%q)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue