diff --git a/cmd/temp_upgrade.go b/cmd/temp_upgrade.go deleted file mode 100644 index 3e412ee58..000000000 --- a/cmd/temp_upgrade.go +++ /dev/null @@ -1,444 +0,0 @@ -package cmd - -import ( - "errors" - "fmt" - "regexp" - "strings" - - "github.com/fatih/color" - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tools/types" - "github.com/spf13/cobra" -) - -// Temporary console command to update the pb_data structure to be compatible with the v0.8.0 changes. -// -// NB! It will be removed in v0.9+ -func NewTempUpgradeCommand(app core.App) *cobra.Command { - command := &cobra.Command{ - Use: "upgrade", - Short: "Upgrades your existing pb_data to be compatible with the v0.8.x changes", - Long: ` -Upgrades your existing pb_data to be compatible with the v0.8.x changes -Prerequisites and caveats: -- already upgraded to v0.7.* -- no existing users collection -- existing profiles collection fields like email, username, verified, etc. will be renamed to username2, email2, etc. -`, - Run: func(command *cobra.Command, args []string) { - if err := upgrade(app); err != nil { - color.Red("Error: %v", err) - } - }, - } - - return command -} - -func upgrade(app core.App) error { - if _, err := app.Dao().FindCollectionByNameOrId("users"); err == nil { - return errors.New("It seems that you've already upgraded or have an existing 'users' collection.") - } - - return app.Dao().RunInTransaction(func(txDao *daos.Dao) error { - if err := migrateCollections(txDao); err != nil { - return err - } - - if err := migrateUsers(app, txDao); err != nil { - return err - } - - if err := resetMigrationsTable(txDao); err != nil { - return err - } - - bold := color.New(color.Bold).Add(color.FgGreen) - bold.Println("The pb_data upgrade completed successfully!") - bold.Println("You can now start the application as usual with the 'serve' command.") - bold.Println("Please review the migrated collection API rules and fields in the Admin UI and apply the necessary changes in your client-side code.") - fmt.Println() - - return nil - }) -} - -// ------------------------------------------------------------------- - -func migrateCollections(txDao *daos.Dao) error { - // add new collection columns - if _, err := txDao.DB().AddColumn("_collections", "type", "TEXT DEFAULT 'base' NOT NULL").Execute(); err != nil { - return err - } - if _, err := txDao.DB().AddColumn("_collections", "options", "JSON DEFAULT '{}' NOT NULL").Execute(); err != nil { - return err - } - - ruleReplacements := []struct { - old string - new string - }{ - {"expand", "expand2"}, - {"collecitonId", "collectionId2"}, - {"collecitonName", "collectionName2"}, - {"profile.userId", "profile.id"}, - - // @collection.* - {"@collection.profiles.userId", "@collection.users.id"}, - {"@collection.profiles.username", "@collection.users.username2"}, - {"@collection.profiles.email", "@collection.users.email2"}, - {"@collection.profiles.emailVisibility", "@collection.users.emailVisibility2"}, - {"@collection.profiles.verified", "@collection.users.verified2"}, - {"@collection.profiles.tokenKey", "@collection.users.tokenKey2"}, - {"@collection.profiles.passwordHash", "@collection.users.passwordHash2"}, - {"@collection.profiles.lastResetSentAt", "@collection.users.lastResetSentAt2"}, - {"@collection.profiles.lastVerificationSentAt", "@collection.users.lastVerificationSentAt2"}, - {"@collection.profiles.", "@collection.users."}, - - // @request.* - {"@request.user.profile.userId", "@request.auth.id"}, - {"@request.user.profile.username", "@request.auth.username2"}, - {"@request.user.profile.email", "@request.auth.email2"}, - {"@request.user.profile.emailVisibility", "@request.auth.emailVisibility2"}, - {"@request.user.profile.verified", "@request.auth.verified2"}, - {"@request.user.profile.tokenKey", "@request.auth.tokenKey2"}, - {"@request.user.profile.passwordHash", "@request.auth.passwordHash2"}, - {"@request.user.profile.lastResetSentAt", "@request.auth.lastResetSentAt2"}, - {"@request.user.profile.lastVerificationSentAt", "@request.auth.lastVerificationSentAt2"}, - {"@request.user.profile.", "@request.auth."}, - {"@request.user", "@request.auth"}, - } - - collections := []*models.Collection{} - if err := txDao.CollectionQuery().All(&collections); err != nil { - return err - } - - for _, collection := range collections { - collection.Type = models.CollectionTypeBase - collection.NormalizeOptions() - - // rename profile fields - // --- - fieldsToRename := []string{ - "collectionId", - "collectionName", - "expand", - } - if collection.Name == "profiles" { - fieldsToRename = append(fieldsToRename, - "username", - "email", - "emailVisibility", - "verified", - "tokenKey", - "passwordHash", - "lastResetSentAt", - "lastVerificationSentAt", - ) - } - for _, name := range fieldsToRename { - f := collection.Schema.GetFieldByName(name) - if f != nil { - color.Blue("[%s - renamed field]", collection.Name) - color.Yellow(" - old: %s", f.Name) - color.Green(" - new: %s2", f.Name) - fmt.Println() - f.Name += "2" - } - } - // --- - - // replace rule fields - // --- - rules := map[string]*string{ - "ListRule": collection.ListRule, - "ViewRule": collection.ViewRule, - "CreateRule": collection.CreateRule, - "UpdateRule": collection.UpdateRule, - "DeleteRule": collection.DeleteRule, - } - - for ruleKey, rule := range rules { - if rule == nil || *rule == "" { - continue - } - - originalRule := *rule - - for _, replacement := range ruleReplacements { - re := regexp.MustCompile(regexp.QuoteMeta(replacement.old) + `\b`) - *rule = re.ReplaceAllString(*rule, replacement.new) - } - - *rule = replaceReversedLikes(*rule) - - if originalRule != *rule { - color.Blue("[%s - replaced %s]:", collection.Name, ruleKey) - color.Yellow(" - old: %s", strings.TrimSpace(originalRule)) - color.Green(" - new: %s", strings.TrimSpace(*rule)) - fmt.Println() - } - } - // --- - - if err := txDao.SaveCollection(collection); err != nil { - return err - } - } - - return nil -} - -func migrateUsers(app core.App, txDao *daos.Dao) error { - color.Blue(`[merging "_users" and "profiles"]:`) - - profilesCollection, err := txDao.FindCollectionByNameOrId("profiles") - if err != nil { - return err - } - - originalProfilesCollectionId := profilesCollection.Id - - // change the profiles collection id to something else since we will be using - // it for the new users collection in order to avoid renaming the storage dir - _, idRenameErr := txDao.DB().NewQuery(fmt.Sprintf( - `UPDATE {{_collections}} - SET id = '%s' - WHERE id = '%s'; - `, - (originalProfilesCollectionId + "__old__"), - originalProfilesCollectionId, - )).Execute() - if idRenameErr != nil { - return idRenameErr - } - - // refresh profiles collection - profilesCollection, err = txDao.FindCollectionByNameOrId("profiles") - if err != nil { - return err - } - - usersSchema, _ := profilesCollection.Schema.Clone() - userIdField := usersSchema.GetFieldByName("userId") - if userIdField != nil { - usersSchema.RemoveField(userIdField.Id) - } - - usersCollection := &models.Collection{} - usersCollection.MarkAsNew() - usersCollection.Id = originalProfilesCollectionId - usersCollection.Name = "users" - usersCollection.Type = models.CollectionTypeAuth - usersCollection.Schema = *usersSchema - usersCollection.CreateRule = types.Pointer("") - if profilesCollection.ListRule != nil && *profilesCollection.ListRule != "" { - *profilesCollection.ListRule = strings.ReplaceAll(*profilesCollection.ListRule, "userId", "id") - usersCollection.ListRule = profilesCollection.ListRule - } - if profilesCollection.ViewRule != nil && *profilesCollection.ViewRule != "" { - *profilesCollection.ViewRule = strings.ReplaceAll(*profilesCollection.ViewRule, "userId", "id") - usersCollection.ViewRule = profilesCollection.ViewRule - } - if profilesCollection.UpdateRule != nil && *profilesCollection.UpdateRule != "" { - *profilesCollection.UpdateRule = strings.ReplaceAll(*profilesCollection.UpdateRule, "userId", "id") - usersCollection.UpdateRule = profilesCollection.UpdateRule - } - if profilesCollection.DeleteRule != nil && *profilesCollection.DeleteRule != "" { - *profilesCollection.DeleteRule = strings.ReplaceAll(*profilesCollection.DeleteRule, "userId", "id") - usersCollection.DeleteRule = profilesCollection.DeleteRule - } - - // set auth options - settings := app.Settings() - authOptions := usersCollection.AuthOptions() - authOptions.ManageRule = nil - authOptions.AllowOAuth2Auth = true - authOptions.AllowUsernameAuth = false - authOptions.AllowEmailAuth = settings.EmailAuth.Enabled - authOptions.MinPasswordLength = settings.EmailAuth.MinPasswordLength - authOptions.OnlyEmailDomains = settings.EmailAuth.OnlyDomains - authOptions.ExceptEmailDomains = settings.EmailAuth.ExceptDomains - // twitter currently is the only provider that doesn't return an email - authOptions.RequireEmail = !settings.TwitterAuth.Enabled - - usersCollection.SetOptions(authOptions) - - if err := txDao.SaveCollection(usersCollection); err != nil { - return err - } - - // copy the original users - _, usersErr := txDao.DB().NewQuery(` - INSERT INTO {{users}} (id, created, updated, username, email, emailVisibility, verified, tokenKey, passwordHash, lastResetSentAt, lastVerificationSentAt) - SELECT id, created, updated, ("u_" || id), email, false, verified, tokenKey, passwordHash, lastResetSentAt, lastVerificationSentAt - FROM {{_users}}; - `).Execute() - if usersErr != nil { - return usersErr - } - - // generate the profile fields copy statements - sets := []string{"id = p.id"} - for _, f := range usersSchema.Fields() { - sets = append(sets, fmt.Sprintf("%s = p.%s", f.Name, f.Name)) - } - - // copy profile fields - _, copyProfileErr := txDao.DB().NewQuery(fmt.Sprintf(` - UPDATE {{users}} as u - SET %s - FROM {{profiles}} as p - WHERE u.id = p.userId; - `, strings.Join(sets, ", "))).Execute() - if copyProfileErr != nil { - return copyProfileErr - } - - profileRecords, err := txDao.FindRecordsByExpr("profiles") - if err != nil { - return err - } - - // update all profiles and users fields to point to the new users collection - collections := []*models.Collection{} - if err := txDao.CollectionQuery().All(&collections); err != nil { - return err - } - for _, collection := range collections { - var hasChanges bool - - for _, f := range collection.Schema.Fields() { - f.InitOptions() - - if f.Type == schema.FieldTypeUser { - if collection.Name == "profiles" && f.Name == "userId" { - continue - } - - hasChanges = true - - // change the user field to a relation field - options, _ := f.Options.(*schema.UserOptions) - f.Type = schema.FieldTypeRelation - f.Options = &schema.RelationOptions{ - CollectionId: usersCollection.Id, - MaxSelect: &options.MaxSelect, - CascadeDelete: options.CascadeDelete, - } - - for _, p := range profileRecords { - pId := p.Id - pUserId := p.GetString("userId") - // replace all user record id references with the profile id - _, replaceErr := txDao.DB().NewQuery(fmt.Sprintf(` - UPDATE %s - SET [[%s]] = REPLACE([[%s]], '%s', '%s') - WHERE [[%s]] LIKE ('%%%s%%'); - `, collection.Name, f.Name, f.Name, pUserId, pId, f.Name, pUserId)).Execute() - if replaceErr != nil { - return replaceErr - } - } - } - } - - if hasChanges { - if err := txDao.Save(collection); err != nil { - return err - } - } - } - - if err := migrateExternalAuths(txDao, originalProfilesCollectionId); err != nil { - return err - } - - // drop _users table - if _, err := txDao.DB().DropTable("_users").Execute(); err != nil { - return err - } - - // drop profiles table - if _, err := txDao.DB().DropTable("profiles").Execute(); err != nil { - return err - } - - // delete profiles collection - if err := txDao.Delete(profilesCollection); err != nil { - return err - } - - color.Green(` - Successfully merged "_users" and "profiles" into a new collection "users".`) - fmt.Println() - - return nil -} - -func migrateExternalAuths(txDao *daos.Dao, userCollectionId string) error { - _, alterErr := txDao.DB().NewQuery(` - -- crate new externalAuths table - CREATE TABLE {{_newExternalAuths}} ( - [[id]] TEXT PRIMARY KEY, - [[collectionId]] TEXT NOT NULL, - [[recordId]] TEXT NOT NULL, - [[provider]] TEXT NOT NULL, - [[providerId]] TEXT NOT NULL, - [[created]] TEXT DEFAULT "" NOT NULL, - [[updated]] TEXT DEFAULT "" NOT NULL, - --- - FOREIGN KEY ([[collectionId]]) REFERENCES {{_collections}} ([[id]]) ON UPDATE CASCADE ON DELETE CASCADE - ); - - -- copy all data from the old table to the new one - INSERT INTO {{_newExternalAuths}} - SELECT auth.id, "` + userCollectionId + `" as collectionId, [[profiles.id]] as recordId, auth.provider, auth.providerId, auth.created, auth.updated - FROM {{_externalAuths}} auth - INNER JOIN {{profiles}} on [[profiles.userId]] = [[auth.userId]]; - - -- drop old table - DROP TABLE {{_externalAuths}}; - - -- rename new table - ALTER TABLE {{_newExternalAuths}} RENAME TO {{_externalAuths}}; - - -- create named indexes - CREATE UNIQUE INDEX _externalAuths_record_provider_idx on {{_externalAuths}} ([[collectionId]], [[recordId]], [[provider]]); - CREATE UNIQUE INDEX _externalAuths_provider_providerId_idx on {{_externalAuths}} ([[provider]], [[providerId]]); - `).Execute() - - return alterErr -} - -func resetMigrationsTable(txDao *daos.Dao) error { - // reset the migration state to the new init - _, err := txDao.DB().Delete("_migrations", dbx.HashExp{ - "file": "1661586591_add_externalAuths_table.go", - }).Execute() - - return err -} - -var reverseLikeRegex = regexp.MustCompile(`(['"]\w*['"])\s*(\~|!~)\s*([\w\@\.]*)`) - -func replaceReversedLikes(rule string) string { - parts := reverseLikeRegex.FindAllStringSubmatch(rule, -1) - - for _, p := range parts { - if len(p) != 4 { - continue - } - - newPart := fmt.Sprintf("%s %s %s", p[3], p[2], p[1]) - - rule = strings.ReplaceAll(rule, p[0], newPart) - } - - return rule -} diff --git a/pocketbase.go b/pocketbase.go index e34fca799..eb1a489ca 100644 --- a/pocketbase.go +++ b/pocketbase.go @@ -136,7 +136,6 @@ func NewWithConfig(config *Config) *PocketBase { func (pb *PocketBase) Start() error { // register system commands pb.RootCmd.AddCommand(cmd.NewAdminCommand(pb)) - pb.RootCmd.AddCommand(cmd.NewTempUpgradeCommand(pb)) pb.RootCmd.AddCommand(cmd.NewServeCommand(pb, !pb.hideStartBanner)) return pb.Execute()