mirror of
https://github.com/pocketbase/pocketbase.git
synced 2025-01-09 10:07:17 +02:00
445 lines
14 KiB
Go
445 lines
14 KiB
Go
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
|
|
}
|