diff --git a/CHANGELOG.md b/CHANGELOG.md index 461cf65b..c19937c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,78 @@ ## (WIP) v0.9.0 -- Changes to the `mailer.Mailer` interface (_minor breaking if you are sending custom emails_): +- Added new event hooks: + ``` + app.OnBeforeBootstrap() + app.OnAfterBootstrap() + ``` + +- Refactored the `migrate` command to support **external JavaScript migration files** using an embedded JS interpreter ([goja](https://github.com/dop251/goja)). + This allow writting custom migration scripts such as programmatically creating collections, + initializing default settings, running import scripts, etc., with a JavaScript API very similar to the Go one (_more documentation will be available soon_). + + The `migrate` command is available by default for the prebult executable, + but if you use PocketBase as framework you need register it manually: + ```go + migrationsDir := "" // default to "pb_migrations" (for js) and "migrations" (for go) + + // load js files if you want to allow loading external JavaScript migrations + jsvm.MustRegisterMigrationsLoader(app, &jsvm.MigrationsLoaderOptions{ + Dir: migrationsDir, + }) + + // init the `migrate` command + migratecmd.MustRegister(app, app.RootCmd, &migratecmd.Options{ + TemplateLang: migratecmd.TemplateLangGo, // or migratecmd.TemplateLangJS + Dir: migrationsDir, + Automigrate: true, + }) + ``` + + **The refactoring also comes with automigrations support.** + + If `Automigrate` is enabled (`true` by default for the prebuilt executable; can be disabled with `--automigrate=0`), + PocketBase will generate seamlessly in the background JS (or Go) migration file with your collection changes. + **The directory with the JS migrations can be committed to your git repo.** + All migrations (Go and JS) are automatically executed on server start. + Also note that the auto generated migrations are granural (in contrast to the `migrate collections` snapshot command) + and allow multiple developers to do changes on the collections independently (even editing the same collection) miniziming the eventual merge conflicts. + Here is a sample JS migration file that will be generated if you for example edit a single collection name: + ```js + // pb_migrations/1669663597_updated_posts_old.js + migrate((db) => { + // up + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("lngf8rb3dqu86r3") + collection.name = "posts_new" + return dao.saveCollection(collection) + }, (db) => { + // down + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("lngf8rb3dqu86r3") + collection.name = "posts_old" + return dao.saveCollection(collection) + }) + ``` + +- Added new Dao helpers to make it easier fetching and updating the app settings from a migration: + ```go + dao.FindSettings([optEncryptionKey]) + dao.SaveSettings(newSettings, [optEncryptionKey]) + ``` + +- Moved `core.Settings` to `models/settings.Settings`: + ``` + core.Settings{} -> settings.Settings{} + core.NewSettings() -> settings.New() + core.MetaConfig{} -> settings.MetaConfig{} + core.LogsConfig{} -> settings.LogsConfig{} + core.SmtpConfig{} -> settings.SmtpConfig{} + core.S3Config{} -> settings.S3Config{} + core.TokenConfig{} -> settings.TokenConfig{} + core.AuthProviderConfig{} -> settings.AuthProviderConfig{} + ``` + +- Changed the `mailer.Mailer` interface (**minor breaking if you are sending custom emails**): ```go // Old: app.NewMailClient().Send(from, to, subject, html, attachments?) @@ -19,8 +91,7 @@ Text: "custom plain text version", }) ``` - -- Added the new `*mailer.Message` to the `MailerRecordEvent`, `MailerAdminEvent` event structs. + The new `*mailer.Message` struct is also now a member of the `MailerRecordEvent` and `MailerAdminEvent` events. ## v0.8.0 diff --git a/plugins/jsvm/vm.go b/plugins/jsvm/vm.go index b2e994b4..915a7428 100644 --- a/plugins/jsvm/vm.go +++ b/plugins/jsvm/vm.go @@ -15,9 +15,15 @@ import ( func NewBaseVM(app core.App) *goja.Runtime { vm := goja.New() vm.SetFieldNameMapper(goja.UncapFieldNameMapper()) - vm.Set("$app", app) + baseBind(vm) + dbxBind(vm) + + return vm +} + +func baseBind(vm *goja.Runtime) { vm.Set("unmarshal", func(src map[string]any, dest any) (any, error) { raw, err := json.Marshal(src) if err != nil { @@ -31,59 +37,41 @@ func NewBaseVM(app core.App) *goja.Runtime { return dest, nil }) - collectionConstructor(vm) - recordConstructor(vm) - adminConstructor(vm) - schemaConstructor(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 schemaConstructor(vm *goja.Runtime) { vm.Set("Schema", func(call goja.ConstructorCall) *goja.Object { instance := &schema.Schema{} instanceValue := vm.ToValue(instance).(*goja.Object) instanceValue.SetPrototype(call.This.Prototype()) return instanceValue }) + vm.Set("SchemaField", func(call goja.ConstructorCall) *goja.Object { instance := &schema.SchemaField{} 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 { @@ -97,7 +85,7 @@ func daoConstructor(vm *goja.Runtime) { }) } -func dbxBinds(vm *goja.Runtime) { +func dbxBind(vm *goja.Runtime) { obj := vm.NewObject() vm.Set("$dbx", obj) @@ -145,7 +133,6 @@ func apisBind(vm *goja.Runtime) { 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) diff --git a/plugins/migratecmd/migratecmd.go b/plugins/migratecmd/migratecmd.go index 53ebfd72..c5bcdcdc 100644 --- a/plugins/migratecmd/migratecmd.go +++ b/plugins/migratecmd/migratecmd.go @@ -95,10 +95,10 @@ func Register(app core.App, rootCmd *cobra.Command, options *Options) error { func (p *plugin) createCommand() *cobra.Command { const cmdDesc = `Supported arguments are: -- up - runs all available migrations -- down [number] - reverts the last [number] applied migrations -- create name [folder] - creates new blank migration template file -- collections [folder] - creates new migration file with the latest local collections snapshot (similar to the automigrate but allows editing) +- up - runs all available migrations +- down [number] - reverts the last [number] applied migrations +- create name - creates new blank migration template file +- collections - creates new migration file with snapshot of the local collections configuration ` command := &cobra.Command{ @@ -143,14 +143,7 @@ func (p *plugin) migrateCreateHandler(template string, args []string) error { } name := args[0] - - var dir string - if len(args) == 2 { - dir = args[1] - } - if dir == "" { - dir = p.options.Dir - } + dir := p.options.Dir resultFilePath := path.Join( dir, diff --git a/plugins/migratecmd/migratecmd_test.go b/plugins/migratecmd/migratecmd_test.go index aab5bc6b..95074a14 100644 --- a/plugins/migratecmd/migratecmd_test.go +++ b/plugins/migratecmd/migratecmd_test.go @@ -164,7 +164,7 @@ func init() { expectedName := "_created_new_name." + s.lang if !strings.Contains(files[0].Name(), expectedName) { - t.Fatalf("Expected filename to contains %q, got %q", expectedName, files[0].Name()) + t.Fatalf("[%d] Expected filename to contains %q, got %q", i, expectedName, files[0].Name()) } fullPath := filepath.Join(migrationsDir, files[0].Name()) @@ -335,7 +335,7 @@ func init() { expectedName := "_deleted_test456." + s.lang if !strings.Contains(files[0].Name(), expectedName) { - t.Fatalf("Expected filename to contains %q, got %q", expectedName, files[0].Name()) + t.Fatalf("[%d] Expected filename to contains %q, got %q", i, expectedName, files[0].Name()) } fullPath := filepath.Join(migrationsDir, files[0].Name()) @@ -681,7 +681,7 @@ func init() { expectedName := "_updated_test456." + s.lang if !strings.Contains(files[0].Name(), expectedName) { - t.Fatalf("Expected filename to contains %q, got %q", expectedName, files[0].Name()) + t.Fatalf("[%d] Expected filename to contains %q, got %q", i, expectedName, files[0].Name()) } fullPath := filepath.Join(migrationsDir, files[0].Name()) diff --git a/plugins/migratecmd/templates.go b/plugins/migratecmd/templates.go index 356af428..26265278 100644 --- a/plugins/migratecmd/templates.go +++ b/plugins/migratecmd/templates.go @@ -648,27 +648,29 @@ func (p *plugin) goDiffTemplate(new *models.Collection, old *models.Collection) up := strings.Join(upParts, "\n\t\t") down := strings.Join(downParts, "\n\t\t") - - var optImports string - combined := up + down + // generate imports + // --- + var imports string + if strings.Contains(combined, "json.Unmarshal(") || strings.Contains(combined, "json.Marshal(") { - optImports += "\n\t\"encoding/json\"\n" + imports += "\n\t\"encoding/json\"\n" } - optImports += "\n\t\"github.com/pocketbase/dbx\"" - optImports += "\n\t\"github.com/pocketbase/pocketbase/daos\"" - optImports += "\n\tm \"github.com/pocketbase/pocketbase/migrations\"" + imports += "\n\t\"github.com/pocketbase/dbx\"" + imports += "\n\t\"github.com/pocketbase/pocketbase/daos\"" + imports += "\n\tm \"github.com/pocketbase/pocketbase/migrations\"" if strings.Contains(combined, "schema.SchemaField{") { - optImports += "\n\t\"github.com/pocketbase/pocketbase/models/schema\"" + imports += "\n\t\"github.com/pocketbase/pocketbase/models/schema\"" } if strings.Contains(combined, "types.Pointer(") { - optImports += "\n\t\"github.com/pocketbase/pocketbase/tools/types\"" + imports += "\n\t\"github.com/pocketbase/pocketbase/tools/types\"" } + // --- const template = `package %s @@ -705,7 +707,7 @@ func init() { return fmt.Sprintf( template, filepath.Base(p.options.Dir), - optImports, + imports, old.Id, strings.TrimSpace(up), new.Id, strings.TrimSpace(down), ), nil