merge v0.23.0-rc changes

This commit is contained in:
Gani Georgiev
2024-09-29 19:23:19 +03:00
parent ad92992324
commit 844f18cac3
753 changed files with 85141 additions and 63396 deletions
+301 -110
View File
@@ -1,26 +1,19 @@
// Package migrations contains the system PocketBase DB migrations.
package migrations
import (
"fmt"
"path/filepath"
"runtime"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/models/settings"
"github.com/pocketbase/pocketbase/tools/migrate"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/types"
)
var AppMigrations migrate.MigrationsList
// Register is a short alias for `AppMigrations.Register()`
// that is usually used in external/user defined migrations.
func Register(
up func(db dbx.Builder) error,
down func(db dbx.Builder) error,
up func(app core.App) error,
down func(app core.App) error,
optFilename ...string,
) {
var optFiles []string
@@ -30,29 +23,28 @@ func Register(
_, path, _, _ := runtime.Caller(1)
optFiles = append(optFiles, filepath.Base(path))
}
AppMigrations.Register(up, down, optFiles...)
core.AppMigrations.Register(up, down, optFiles...)
}
func init() {
AppMigrations.Register(func(db dbx.Builder) error {
_, tablesErr := db.NewQuery(`
CREATE TABLE {{_admins}} (
[[id]] TEXT PRIMARY KEY NOT NULL,
[[avatar]] INTEGER DEFAULT 0 NOT NULL,
[[email]] TEXT UNIQUE NOT NULL,
[[tokenKey]] TEXT UNIQUE NOT NULL,
[[passwordHash]] TEXT NOT NULL,
[[lastResetSentAt]] TEXT DEFAULT "" NOT NULL,
[[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
[[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL
);
core.SystemMigrations.Register(func(txApp core.App) error {
if err := createLogsTable(txApp); err != nil {
return fmt.Errorf("_logs error: %w", err)
}
if err := createParamsTable(txApp); err != nil {
return fmt.Errorf("_params exec error: %w", err)
}
// -----------------------------------------------------------
_, execerr := txApp.DB().NewQuery(`
CREATE TABLE {{_collections}} (
[[id]] TEXT PRIMARY KEY NOT NULL,
[[id]] TEXT PRIMARY KEY DEFAULT ('r'||lower(hex(randomblob(7)))) NOT NULL,
[[system]] BOOLEAN DEFAULT FALSE NOT NULL,
[[type]] TEXT DEFAULT "base" NOT NULL,
[[name]] TEXT UNIQUE NOT NULL,
[[schema]] JSON DEFAULT "[]" NOT NULL,
[[fields]] JSON DEFAULT "[]" NOT NULL,
[[indexes]] JSON DEFAULT "[]" NOT NULL,
[[listRule]] TEXT DEFAULT NULL,
[[viewRule]] TEXT DEFAULT NULL,
@@ -63,104 +55,54 @@ func init() {
[[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
[[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL
);
CREATE TABLE {{_params}} (
[[id]] TEXT PRIMARY KEY NOT NULL,
[[key]] TEXT UNIQUE NOT NULL,
[[value]] JSON DEFAULT NULL,
[[created]] TEXT DEFAULT "" NOT NULL,
[[updated]] TEXT DEFAULT "" NOT NULL
);
CREATE TABLE {{_externalAuths}} (
[[id]] TEXT PRIMARY KEY NOT NULL,
[[collectionId]] TEXT NOT NULL,
[[recordId]] TEXT NOT NULL,
[[provider]] TEXT NOT NULL,
[[providerId]] TEXT NOT NULL,
[[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
[[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
---
FOREIGN KEY ([[collectionId]]) REFERENCES {{_collections}} ([[id]]) ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE UNIQUE INDEX _externalAuths_record_provider_idx on {{_externalAuths}} ([[collectionId]], [[recordId]], [[provider]]);
CREATE UNIQUE INDEX _externalAuths_collection_provider_idx on {{_externalAuths}} ([[collectionId]], [[provider]], [[providerId]]);
`).Execute()
if tablesErr != nil {
return tablesErr
if execerr != nil {
return fmt.Errorf("_collections exec error: %w", execerr)
}
dao := daos.New(db)
if err := createMFAsCollection(txApp); err != nil {
return fmt.Errorf("_mfas error: %w", err)
}
// inserts default settings
// -----------------------------------------------------------
defaultSettings := settings.New()
if err := dao.SaveSettings(defaultSettings); err != nil {
if err := createOTPsCollection(txApp); err != nil {
return fmt.Errorf("_otps error: %w", err)
}
if err := createExternalAuthsCollection(txApp); err != nil {
return fmt.Errorf("_externalAuths error: %w", err)
}
if err := createAuthOriginsCollection(txApp); err != nil {
return fmt.Errorf("_authOrigins error: %w", err)
}
if err := createSuperusersCollection(txApp); err != nil {
return fmt.Errorf("_superusers error: %w", err)
}
if err := createUsersCollection(txApp); err != nil {
return fmt.Errorf("users error: %w", err)
}
return nil
}, func(txApp core.App) error {
_, err := txApp.AuxDB().DropTable("_logs").Execute()
if err != nil {
return err
}
// inserts the system users collection
// -----------------------------------------------------------
usersCollection := &models.Collection{}
usersCollection.MarkAsNew()
usersCollection.Id = "_pb_users_auth_"
usersCollection.Name = "users"
usersCollection.Type = models.CollectionTypeAuth
usersCollection.ListRule = types.Pointer("id = @request.auth.id")
usersCollection.ViewRule = types.Pointer("id = @request.auth.id")
usersCollection.CreateRule = types.Pointer("")
usersCollection.UpdateRule = types.Pointer("id = @request.auth.id")
usersCollection.DeleteRule = types.Pointer("id = @request.auth.id")
// set auth options
usersCollection.SetOptions(models.CollectionAuthOptions{
ManageRule: nil,
AllowOAuth2Auth: true,
AllowUsernameAuth: true,
AllowEmailAuth: true,
MinPasswordLength: 8,
RequireEmail: false,
})
// set optional default fields
usersCollection.Schema = schema.NewSchema(
&schema.SchemaField{
Id: "users_name",
Type: schema.FieldTypeText,
Name: "name",
Options: &schema.TextOptions{},
},
&schema.SchemaField{
Id: "users_avatar",
Type: schema.FieldTypeFile,
Name: "avatar",
Options: &schema.FileOptions{
MaxSelect: 1,
MaxSize: 5242880,
MimeTypes: []string{
"image/jpeg",
"image/png",
"image/svg+xml",
"image/gif",
"image/webp",
},
},
},
)
return dao.SaveCollection(usersCollection)
}, func(db dbx.Builder) error {
tables := []string{
"users",
"_externalAuths",
core.CollectionNameSuperusers,
core.CollectionNameMFAs,
core.CollectionNameOTPs,
core.CollectionNameAuthOrigins,
"_params",
"_collections",
"_admins",
}
for _, name := range tables {
if _, err := db.DropTable(name).Execute(); err != nil {
if _, err := txApp.DB().DropTable(name).Execute(); err != nil {
return err
}
}
@@ -168,3 +110,252 @@ func init() {
return nil
})
}
func createParamsTable(txApp core.App) error {
_, execErr := txApp.DB().NewQuery(`
CREATE TABLE {{_params}} (
[[id]] TEXT PRIMARY KEY DEFAULT ('r'||lower(hex(randomblob(7)))) NOT NULL,
[[value]] JSON DEFAULT NULL,
[[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
[[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL
);
`).Execute()
return execErr
}
func createLogsTable(txApp core.App) error {
_, execErr := txApp.AuxDB().NewQuery(`
CREATE TABLE {{_logs}} (
[[id]] TEXT PRIMARY KEY DEFAULT ('r'||lower(hex(randomblob(7)))) NOT NULL,
[[level]] INTEGER DEFAULT 0 NOT NULL,
[[message]] TEXT DEFAULT "" NOT NULL,
[[data]] JSON DEFAULT "{}" NOT NULL,
[[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL
);
CREATE INDEX idx_logs_level on {{_logs}} ([[level]]);
CREATE INDEX idx_logs_message on {{_logs}} ([[message]]);
CREATE INDEX idx_logs_created_hour on {{_logs}} (strftime('%Y-%m-%d %H:00:00', [[created]]));
`).Execute()
return execErr
}
func createMFAsCollection(txApp core.App) error {
col := core.NewBaseCollection(core.CollectionNameMFAs)
col.System = true
ownerRule := "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId"
col.ListRule = types.Pointer(ownerRule)
col.ViewRule = types.Pointer(ownerRule)
col.DeleteRule = types.Pointer(ownerRule)
col.Fields.Add(&core.TextField{
Name: "collectionRef",
System: true,
Required: true,
})
col.Fields.Add(&core.TextField{
Name: "recordRef",
System: true,
Required: true,
})
col.Fields.Add(&core.TextField{
Name: "method",
System: true,
Required: true,
})
col.Fields.Add(&core.AutodateField{
Name: "created",
System: true,
OnCreate: true,
})
col.Fields.Add(&core.AutodateField{
Name: "updated",
System: true,
OnCreate: true,
OnUpdate: true,
})
col.AddIndex("idx_mfas_collectionRef_recordRef", false, "collectionRef,recordRef", "")
return txApp.Save(col)
}
func createOTPsCollection(txApp core.App) error {
col := core.NewBaseCollection(core.CollectionNameOTPs)
col.System = true
ownerRule := "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId"
col.ListRule = types.Pointer(ownerRule)
col.ViewRule = types.Pointer(ownerRule)
col.DeleteRule = types.Pointer(ownerRule)
col.Fields.Add(&core.TextField{
Name: "collectionRef",
System: true,
Required: true,
})
col.Fields.Add(&core.TextField{
Name: "recordRef",
System: true,
Required: true,
})
col.Fields.Add(&core.PasswordField{
Name: "password",
System: true,
Hidden: true,
Required: true,
Cost: 8, // low cost for better performce and because it is not critical
})
col.Fields.Add(&core.AutodateField{
Name: "created",
System: true,
OnCreate: true,
})
col.Fields.Add(&core.AutodateField{
Name: "updated",
System: true,
OnCreate: true,
OnUpdate: true,
})
col.AddIndex("idx_otps_collectionRef_recordRef", false, "collectionRef, recordRef", "")
return txApp.Save(col)
}
func createAuthOriginsCollection(txApp core.App) error {
col := core.NewBaseCollection(core.CollectionNameAuthOrigins)
col.System = true
ownerRule := "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId"
col.ListRule = types.Pointer(ownerRule)
col.ViewRule = types.Pointer(ownerRule)
col.DeleteRule = types.Pointer(ownerRule)
col.Fields.Add(&core.TextField{
Name: "collectionRef",
System: true,
Required: true,
})
col.Fields.Add(&core.TextField{
Name: "recordRef",
System: true,
Required: true,
})
col.Fields.Add(&core.TextField{
Name: "fingerprint",
System: true,
Required: true,
})
col.Fields.Add(&core.AutodateField{
Name: "created",
System: true,
OnCreate: true,
})
col.Fields.Add(&core.AutodateField{
Name: "updated",
System: true,
OnCreate: true,
OnUpdate: true,
})
col.AddIndex("idx_authOrigins_unique_pairs", true, "collectionRef, recordRef, fingerprint", "")
return txApp.Save(col)
}
func createExternalAuthsCollection(txApp core.App) error {
col := core.NewBaseCollection(core.CollectionNameExternalAuths)
col.System = true
ownerRule := "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId"
col.ListRule = types.Pointer(ownerRule)
col.ViewRule = types.Pointer(ownerRule)
col.DeleteRule = types.Pointer(ownerRule)
col.Fields.Add(&core.TextField{
Name: "collectionRef",
System: true,
Required: true,
})
col.Fields.Add(&core.TextField{
Name: "recordRef",
System: true,
Required: true,
})
col.Fields.Add(&core.TextField{
Name: "provider",
System: true,
Required: true,
})
col.Fields.Add(&core.TextField{
Name: "providerId",
System: true,
Required: true,
})
col.Fields.Add(&core.AutodateField{
Name: "created",
System: true,
OnCreate: true,
})
col.Fields.Add(&core.AutodateField{
Name: "updated",
System: true,
OnCreate: true,
OnUpdate: true,
})
col.AddIndex("idx_externalAuths_record_provider", true, "collectionRef, recordRef, provider", "")
col.AddIndex("idx_externalAuths_collection_provider", true, "collectionRef, provider, providerId", "")
return txApp.Save(col)
}
func createSuperusersCollection(txApp core.App) error {
superusers := core.NewAuthCollection(core.CollectionNameSuperusers)
superusers.System = true
superusers.Fields.Add(&core.EmailField{
Name: "email",
System: true,
Required: true,
})
superusers.Fields.Add(&core.AutodateField{
Name: "created",
System: true,
OnCreate: true,
})
superusers.Fields.Add(&core.AutodateField{
Name: "updated",
System: true,
OnCreate: true,
OnUpdate: true,
})
superusers.AuthToken.Duration = 86400 // 1 day
return txApp.Save(superusers)
}
func createUsersCollection(txApp core.App) error {
users := core.NewAuthCollection("users")
users.Fields.Add(&core.TextField{
Name: "name",
Max: 255,
})
users.Fields.Add(&core.FileField{
Name: "avatar",
MaxSelect: 1,
MimeTypes: []string{"image/jpeg", "image/png", "image/svg+xml", "image/gif", "image/webp"},
})
users.Fields.Add(&core.AutodateField{
Name: "created",
OnCreate: true,
})
users.Fields.Add(&core.AutodateField{
Name: "updated",
OnCreate: true,
OnUpdate: true,
})
users.OAuth2.MappedFields.Name = "name"
users.OAuth2.MappedFields.AvatarURL = "avatar"
return txApp.Save(users)
}
@@ -1,215 +0,0 @@
package migrations
import (
"regexp"
"strings"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
)
// This migration replaces for backward compatibility the default operators
// (=, !=, >, etc.) with their any/opt equivalent (?=, ?=, ?>, etc.)
// in any muli-rel expression collection rule.
func init() {
AppMigrations.Register(func(db dbx.Builder) error {
dao := daos.New(db)
exprRegex := regexp.MustCompile(`([\@\'\"\w\.]+)\s*(=|!=|~|!~|>|>=|<|<=)\s*([\@\'\"\w\.]+)`)
collections := []*models.Collection{}
if err := dao.CollectionQuery().All(&collections); err != nil {
return err
}
findCollection := func(nameOrId string) *models.Collection {
for _, c := range collections {
if c.Id == nameOrId || c.Name == nameOrId {
return c
}
}
return nil
}
var isMultiRelLiteral func(mainCollection *models.Collection, literal string) bool
isMultiRelLiteral = func(mainCollection *models.Collection, literal string) bool {
if strings.HasPrefix(literal, "@collection.") {
return true
}
if strings.HasPrefix(literal, `"`) ||
strings.HasPrefix(literal, `'`) ||
strings.HasPrefix(literal, "@request.method") ||
strings.HasPrefix(literal, "@request.data") ||
strings.HasPrefix(literal, "@request.query") {
return false
}
parts := strings.Split(literal, ".")
if len(parts) <= 1 {
return false
}
if strings.HasPrefix(literal, "@request.auth") && len(parts) >= 4 {
// check each auth collection
for _, c := range collections {
if c.IsAuth() && isMultiRelLiteral(c, strings.Join(parts[2:], ".")) {
return true
}
}
return false
}
activeCollection := mainCollection
for i, p := range parts {
f := activeCollection.Schema.GetFieldByName(p)
if f == nil || f.Type != schema.FieldTypeRelation {
return false // not a relation field
}
// is multi-relation and not the last prop
opt, ok := f.Options.(*schema.RelationOptions)
if ok && (opt.MaxSelect == nil || *opt.MaxSelect != 1) && i != len(parts)-1 {
return true
}
activeCollection = findCollection(opt.CollectionId)
if activeCollection == nil {
return false
}
}
return false
}
// replace all multi-match operators to their any/opt equivalent, eg. "=" => "?="
migrateRule := func(collection *models.Collection, rule *string) (*string, error) {
if rule == nil || *rule == "" {
return rule, nil
}
newRule := *rule
parts := exprRegex.FindAllStringSubmatch(newRule, -1)
for _, p := range parts {
if isMultiRelLiteral(collection, p[1]) || isMultiRelLiteral(collection, p[3]) {
newRule = strings.ReplaceAll(newRule, p[0], p[1]+" ?"+p[2]+" "+p[3])
}
}
return &newRule, nil
}
var ruleErr error
for _, c := range collections {
c.ListRule, ruleErr = migrateRule(c, c.ListRule)
if ruleErr != nil {
return ruleErr
}
c.ViewRule, ruleErr = migrateRule(c, c.ViewRule)
if ruleErr != nil {
return ruleErr
}
c.CreateRule, ruleErr = migrateRule(c, c.CreateRule)
if ruleErr != nil {
return ruleErr
}
c.UpdateRule, ruleErr = migrateRule(c, c.UpdateRule)
if ruleErr != nil {
return ruleErr
}
c.DeleteRule, ruleErr = migrateRule(c, c.DeleteRule)
if ruleErr != nil {
return ruleErr
}
if c.IsAuth() {
opt := c.AuthOptions()
opt.ManageRule, ruleErr = migrateRule(c, opt.ManageRule)
if ruleErr != nil {
return ruleErr
}
c.SetOptions(opt)
}
if err := dao.Save(c); err != nil {
return err
}
}
return nil
}, func(db dbx.Builder) error {
dao := daos.New(db)
collections := []*models.Collection{}
if err := dao.CollectionQuery().All(&collections); err != nil {
return err
}
anyOpRegex := regexp.MustCompile(`\?(=|!=|~|!~|>|>=|<|<=)`)
// replace any/opt operators to their old versions, eg. "?=" => "="
revertRule := func(rule *string) (*string, error) {
if rule == nil || *rule == "" {
return rule, nil
}
newRule := *rule
newRule = anyOpRegex.ReplaceAllString(newRule, "${1}")
return &newRule, nil
}
var ruleErr error
for _, c := range collections {
c.ListRule, ruleErr = revertRule(c.ListRule)
if ruleErr != nil {
return ruleErr
}
c.ViewRule, ruleErr = revertRule(c.ViewRule)
if ruleErr != nil {
return ruleErr
}
c.CreateRule, ruleErr = revertRule(c.CreateRule)
if ruleErr != nil {
return ruleErr
}
c.UpdateRule, ruleErr = revertRule(c.UpdateRule)
if ruleErr != nil {
return ruleErr
}
c.DeleteRule, ruleErr = revertRule(c.DeleteRule)
if ruleErr != nil {
return ruleErr
}
if c.IsAuth() {
opt := c.AuthOptions()
opt.ManageRule, ruleErr = revertRule(opt.ManageRule)
if ruleErr != nil {
return ruleErr
}
c.SetOptions(opt)
}
if err := dao.Save(c); err != nil {
return err
}
}
return nil
})
}
@@ -1,26 +0,0 @@
package migrations
import (
"github.com/pocketbase/dbx"
)
// This migration replaces the "authentikAuth" setting with "oidc".
func init() {
AppMigrations.Register(func(db dbx.Builder) error {
_, err := db.NewQuery(`
UPDATE {{_params}}
SET [[value]] = replace([[value]], '"authentikAuth":', '"oidcAuth":')
WHERE [[key]] = 'settings'
`).Execute()
return err
}, func(db dbx.Builder) error {
_, err := db.NewQuery(`
UPDATE {{_params}}
SET [[value]] = replace([[value]], '"oidcAuth":', '"authentikAuth":')
WHERE [[key]] = 'settings'
`).Execute()
return err
})
}
@@ -1,108 +0,0 @@
package migrations
import (
"fmt"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
)
// Normalizes old single and multiple values of MultiValuer fields (file, select, relation).
func init() {
AppMigrations.Register(func(db dbx.Builder) error {
return normalizeMultivaluerFields(db)
}, func(db dbx.Builder) error {
return nil
})
}
func normalizeMultivaluerFields(db dbx.Builder) error {
dao := daos.New(db)
collections := []*models.Collection{}
if err := dao.CollectionQuery().All(&collections); err != nil {
return err
}
for _, c := range collections {
if c.IsView() {
// skip view collections
continue
}
for _, f := range c.Schema.Fields() {
opt, ok := f.Options.(schema.MultiValuer)
if !ok {
continue
}
var updateQuery *dbx.Query
if opt.IsMultiple() {
updateQuery = dao.DB().NewQuery(fmt.Sprintf(
`UPDATE {{%s}} set [[%s]] = (
CASE
WHEN COALESCE([[%s]], '') = ''
THEN '[]'
ELSE (
CASE
WHEN json_valid([[%s]]) AND json_type([[%s]]) == 'array'
THEN [[%s]]
ELSE json_array([[%s]])
END
)
END
)`,
c.Name,
f.Name,
f.Name,
f.Name,
f.Name,
f.Name,
f.Name,
))
} else {
updateQuery = dao.DB().NewQuery(fmt.Sprintf(
`UPDATE {{%s}} set [[%s]] = (
CASE
WHEN COALESCE([[%s]], '[]') = '[]'
THEN ''
ELSE (
CASE
WHEN json_valid([[%s]]) AND json_type([[%s]]) == 'array'
THEN COALESCE(json_extract([[%s]], '$[#-1]'), '')
ELSE [[%s]]
END
)
END
)`,
c.Name,
f.Name,
f.Name,
f.Name,
f.Name,
f.Name,
f.Name,
))
}
if _, err := updateQuery.Execute(); err != nil {
return err
}
}
}
// trigger view query update after the records normalization
// (ignore save error in case of invalid query to allow users to change it from the UI)
for _, c := range collections {
if !c.IsView() {
continue
}
dao.SaveCollection(c)
}
return nil
}
-141
View File
@@ -1,141 +0,0 @@
package migrations
import (
"fmt"
"strings"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tools/dbutils"
"github.com/pocketbase/pocketbase/tools/list"
)
// Adds _collections indexes column (if not already).
//
// Note: This migration will be deleted once schema.SchemaField.Unuique is removed.
func init() {
AppMigrations.Register(func(db dbx.Builder) error {
dao := daos.New(db)
// cleanup failed remaining/"dangling" temp views to prevent
// errors during the indexes upsert
// ---
tempViews := []string{}
viewsErr := db.Select("name").
From("sqlite_schema").
AndWhere(dbx.HashExp{"type": "view"}).
AndWhere(dbx.NewExp(`[[name]] LIKE '\_temp\_%' ESCAPE '\'`)).
Column(&tempViews)
if viewsErr != nil {
return viewsErr
}
for _, name := range tempViews {
if err := dao.DeleteView(name); err != nil {
return err
}
}
// ---
cols, err := dao.TableColumns("_collections")
if err != nil {
return err
}
var hasIndexesColumn bool
for _, col := range cols {
if col == "indexes" {
// already existing (probably via the init migration)
hasIndexesColumn = true
break
}
}
if !hasIndexesColumn {
if _, err := db.AddColumn("_collections", "indexes", `JSON DEFAULT "[]" NOT NULL`).Execute(); err != nil {
return err
}
}
collections := []*models.Collection{}
if err := dao.CollectionQuery().AndWhere(dbx.NewExp("type != 'view'")).All(&collections); err != nil {
return err
}
type indexInfo struct {
Sql string `db:"sql"`
IndexName string `db:"name"`
TableName string `db:"tbl_name"`
}
indexesQuery := db.NewQuery(`SELECT * FROM sqlite_master WHERE type = "index" and sql is not null`)
rawIndexes := []indexInfo{}
if err := indexesQuery.All(&rawIndexes); err != nil {
return err
}
indexesByTableName := map[string][]indexInfo{}
for _, idx := range rawIndexes {
indexesByTableName[idx.TableName] = append(indexesByTableName[idx.TableName], idx)
}
for _, c := range collections {
c.Indexes = nil // reset
excludeIndexes := []string{
"_" + c.Id + "_email_idx",
"_" + c.Id + "_username_idx",
"_" + c.Id + "_tokenKey_idx",
}
// convert custom indexes into the related collections
for _, idx := range indexesByTableName[c.Name] {
if strings.Contains(idx.IndexName, "sqlite_autoindex_") ||
list.ExistInSlice(idx.IndexName, excludeIndexes) {
continue
}
// drop old index (it will be recreated with the collection)
if _, err := db.DropIndex(idx.TableName, idx.IndexName).Execute(); err != nil {
return err
}
c.Indexes = append(c.Indexes, idx.Sql)
}
// convert unique fields to indexes
FieldsLoop:
for _, f := range c.Schema.Fields() {
if !f.Unique {
continue
}
for _, idx := range indexesByTableName[c.Name] {
parsed := dbutils.ParseIndex(idx.Sql)
if parsed.Unique && len(parsed.Columns) == 1 && strings.EqualFold(parsed.Columns[0].Name, f.Name) {
continue FieldsLoop // already added
}
}
c.Indexes = append(c.Indexes, fmt.Sprintf(
`CREATE UNIQUE INDEX "idx_unique_%s" on "%s" ("%s")`,
f.Id,
c.Name,
f.Name,
))
}
if len(c.Indexes) > 0 {
if err := dao.SaveCollection(c); err != nil {
return err
}
}
}
return nil
}, func(db dbx.Builder) error {
_, err := db.DropColumn("_collections", "indexes").Execute()
return err
})
}
-20
View File
@@ -1,20 +0,0 @@
package migrations
import (
"github.com/pocketbase/dbx"
)
// Cleanup dangling deleted collections references
// (see https://github.com/pocketbase/pocketbase/discussions/2570).
func init() {
AppMigrations.Register(func(db dbx.Builder) error {
_, err := db.NewQuery(`
DELETE FROM {{_externalAuths}}
WHERE [[collectionId]] NOT IN (SELECT [[id]] FROM {{_collections}})
`).Execute()
return err
}, func(db dbx.Builder) error {
return nil
})
}
@@ -1,15 +0,0 @@
package migrations
import (
"github.com/pocketbase/dbx"
)
// Renormalizes old single and multiple values of MultiValuer fields (file, select, relation)
// (see https://github.com/pocketbase/pocketbase/issues/2930).
func init() {
AppMigrations.Register(func(db dbx.Builder) error {
return normalizeMultivaluerFields(db)
}, func(db dbx.Builder) error {
return nil
})
}
@@ -1,58 +0,0 @@
package migrations
import (
"fmt"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
)
// Reset all previously inserted NULL values to the fields zero-default.
func init() {
AppMigrations.Register(func(db dbx.Builder) error {
dao := daos.New(db)
collections := []*models.Collection{}
if err := dao.CollectionQuery().All(&collections); err != nil {
return err
}
for _, collection := range collections {
if collection.IsView() {
continue
}
for _, f := range collection.Schema.Fields() {
defaultVal := "''"
switch f.Type {
case schema.FieldTypeJson:
continue
case schema.FieldTypeBool:
defaultVal = "FALSE"
case schema.FieldTypeNumber:
defaultVal = "0"
default:
if opt, ok := f.Options.(schema.MultiValuer); ok && opt.IsMultiple() {
defaultVal = "'[]'"
}
}
_, err := db.NewQuery(fmt.Sprintf(
"UPDATE {{%s}} SET [[%s]] = %s WHERE [[%s]] IS NULL",
collection.Name,
f.Name,
defaultVal,
f.Name,
)).Execute()
if err != nil {
return err
}
}
}
return nil
}, nil)
}
@@ -1,61 +0,0 @@
package migrations
import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
)
// Transform the relation fields to views from non-view collections to json or text fields
// (see https://github.com/pocketbase/pocketbase/issues/3000).
func init() {
AppMigrations.Register(func(db dbx.Builder) error {
dao := daos.New(db)
views, err := dao.FindCollectionsByType(models.CollectionTypeView)
if err != nil {
return err
}
for _, view := range views {
refs, err := dao.FindCollectionReferences(view)
if err != nil {
return nil
}
for collection, fields := range refs {
if collection.IsView() {
continue // view-view relations are allowed
}
for _, f := range fields {
opt, ok := f.Options.(schema.MultiValuer)
if !ok {
continue
}
if opt.IsMultiple() {
f.Type = schema.FieldTypeJson
f.Options = &schema.JsonOptions{}
} else {
f.Type = schema.FieldTypeText
f.Options = &schema.TextOptions{}
}
// replace the existing field
// (this usually is not necessary since it is a pointer,
// but it is better to be explicit in case FindCollectionReferences changes)
collection.Schema.AddField(f)
}
// "raw" save without records table sync
if err := dao.Save(collection); err != nil {
return err
}
}
}
return nil
}, nil)
}
-28
View File
@@ -1,28 +0,0 @@
package migrations
import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
)
// Resave all view collections to ensure that the proper id normalization is applied.
// (see https://github.com/pocketbase/pocketbase/issues/3110)
func init() {
AppMigrations.Register(func(db dbx.Builder) error {
dao := daos.New(db)
collections, err := dao.FindCollectionsByType(models.CollectionTypeView)
if err != nil {
return nil
}
for _, collection := range collections {
// ignore errors to allow users to adjust
// the view queries after app start
dao.SaveCollection(collection)
}
return nil
}, nil)
}
@@ -1,62 +0,0 @@
package migrations
import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
)
// Copy the now deprecated RelationOptions.DisplayFields values from
// all relation fields and register its value as Presentable under
// the specific field in the related collection.
//
// If there is more than one relation to a single collection with explicitly
// set DisplayFields only one of the configuration will be copied.
func init() {
AppMigrations.Register(func(db dbx.Builder) error {
dao := daos.New(db)
collections := []*models.Collection{}
if err := dao.CollectionQuery().All(&collections); err != nil {
return err
}
indexedCollections := make(map[string]*models.Collection, len(collections))
for _, collection := range collections {
indexedCollections[collection.Id] = collection
}
for _, collection := range indexedCollections {
for _, f := range collection.Schema.Fields() {
if f.Type != schema.FieldTypeRelation {
continue
}
options, ok := f.Options.(*schema.RelationOptions)
if !ok || len(options.DisplayFields) == 0 {
continue
}
relCollection, ok := indexedCollections[options.CollectionId]
if !ok {
continue
}
for _, name := range options.DisplayFields {
relField := relCollection.Schema.GetFieldByName(name)
if relField != nil {
relField.Presentable = true
}
}
// only raw model save
if err := dao.Save(relCollection); err != nil {
return err
}
}
}
return nil
}, nil)
}
@@ -1,23 +0,0 @@
package migrations
import (
"github.com/pocketbase/dbx"
)
// Fixes the unique _externalAuths constraint for old installations
// to allow a single OAuth2 provider to be registered for different auth collections.
func init() {
AppMigrations.Register(func(db dbx.Builder) error {
_, createErr := db.NewQuery("CREATE UNIQUE INDEX IF NOT EXISTS _externalAuths_collection_provider_idx on {{_externalAuths}} ([[collectionId]], [[provider]], [[providerId]])").Execute()
if createErr != nil {
return createErr
}
_, dropErr := db.NewQuery("DROP INDEX IF EXISTS _externalAuths_provider_providerId_idx").Execute()
if dropErr != nil {
return dropErr
}
return nil
}, nil)
}
@@ -1,51 +0,0 @@
package migrations
import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
)
// Update all collections with json fields to have a default MaxSize json field option.
func init() {
AppMigrations.Register(func(db dbx.Builder) error {
dao := daos.New(db)
// note: update even the view collections to prevent
// unnecessary change detections during the automigrate
collections := []*models.Collection{}
if err := dao.CollectionQuery().All(&collections); err != nil {
return err
}
for _, collection := range collections {
var needSave bool
for _, f := range collection.Schema.Fields() {
if f.Type != schema.FieldTypeJson {
continue
}
options, _ := f.Options.(*schema.JsonOptions)
if options == nil {
options = &schema.JsonOptions{}
}
options.MaxSize = 2000000 // 2mb
f.Options = options
needSave = true
}
if !needSave {
continue
}
// save only the collection model without updating its records table
if err := dao.Save(collection); err != nil {
return err
}
}
return nil
}, nil)
}
+912
View File
@@ -0,0 +1,912 @@
package migrations
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/pocketbase/pocketbase/tools/types"
"github.com/spf13/cast"
"golang.org/x/crypto/bcrypt"
)
// note: this migration will be deleted in future version
func init() {
core.SystemMigrations.Register(func(txApp core.App) error {
// note: mfas and authOrigins tables are available only with v0.23
hasUpgraded := txApp.HasTable(core.CollectionNameMFAs) && txApp.HasTable(core.CollectionNameAuthOrigins)
if hasUpgraded {
return nil
}
oldSettings, err := loadOldSettings(txApp)
if err != nil {
return fmt.Errorf("failed to fetch old settings: %w", err)
}
if err = migrateOldCollections(txApp, oldSettings); err != nil {
return err
}
if err = migrateSuperusers(txApp, oldSettings); err != nil {
return fmt.Errorf("failed to migrate admins->superusers: %w", err)
}
if err = migrateSettings(txApp, oldSettings); err != nil {
return fmt.Errorf("failed to migrate settings: %w", err)
}
if err = migrateExternalAuths(txApp); err != nil {
return fmt.Errorf("failed to migrate externalAuths: %w", err)
}
if err = createMFAsCollection(txApp); err != nil {
return fmt.Errorf("failed to create mfas collection: %w", err)
}
if err = createOTPsCollection(txApp); err != nil {
return fmt.Errorf("failed to create otps collection: %w", err)
}
if err = createAuthOriginsCollection(txApp); err != nil {
return fmt.Errorf("failed to create authOrigins collection: %w", err)
}
if err = createLogsTable(txApp); err != nil {
return fmt.Errorf("failed tocreate logs table: %w", err)
}
if err = os.Remove(filepath.Join(txApp.DataDir(), "logs.db")); err != nil {
txApp.Logger().Warn("Failed to delete old logs.db file")
}
return nil
}, nil)
}
// -------------------------------------------------------------------
func migrateSuperusers(txApp core.App, oldSettings *oldSettingsModel) error {
// create new superusers collection and table
err := createSuperusersCollection(txApp)
if err != nil {
return err
}
// update with the token options from the old settings
superusersCollection, err := txApp.FindCollectionByNameOrId(core.CollectionNameSuperusers)
if err != nil {
return err
}
superusersCollection.AuthToken.Secret = zeroFallback(
cast.ToString(getMapVal(oldSettings.Value, "adminAuthToken", "secret")),
superusersCollection.AuthToken.Secret,
)
superusersCollection.AuthToken.Duration = zeroFallback(
cast.ToInt64(getMapVal(oldSettings.Value, "adminAuthToken", "duration")),
superusersCollection.AuthToken.Duration,
)
superusersCollection.PasswordResetToken.Secret = zeroFallback(
cast.ToString(getMapVal(oldSettings.Value, "adminPasswordResetToken", "secret")),
superusersCollection.PasswordResetToken.Secret,
)
superusersCollection.PasswordResetToken.Duration = zeroFallback(
cast.ToInt64(getMapVal(oldSettings.Value, "adminPasswordResetToken", "duration")),
superusersCollection.PasswordResetToken.Duration,
)
superusersCollection.FileToken.Secret = zeroFallback(
cast.ToString(getMapVal(oldSettings.Value, "adminFileToken", "secret")),
superusersCollection.FileToken.Secret,
)
superusersCollection.FileToken.Duration = zeroFallback(
cast.ToInt64(getMapVal(oldSettings.Value, "adminFileToken", "duration")),
superusersCollection.FileToken.Duration,
)
if err = txApp.Save(superusersCollection); err != nil {
return fmt.Errorf("failed to migrate token configs: %w", err)
}
// copy old admins records into the new one
_, err = txApp.DB().NewQuery(`
INSERT INTO {{` + core.CollectionNameSuperusers + `}} ([[id]], [[verified]], [[email]], [[password]], [[tokenKey]], [[created]], [[updated]])
SELECT [[id]], true, [[email]], [[passwordHash]], [[tokenKey]], [[created]], [[updated]] FROM {{_admins}};
`).Execute()
if err != nil {
return err
}
// remove old admins table
_, err = txApp.DB().DropTable("_admins").Execute()
if err != nil {
return err
}
return nil
}
// -------------------------------------------------------------------
type oldSettingsModel struct {
Id string `db:"id" json:"id"`
Key string `db:"key" json:"key"`
RawValue types.JSONRaw `db:"value" json:"value"`
Value map[string]any `db:"-" json:"-"`
}
func loadOldSettings(txApp core.App) (*oldSettingsModel, error) {
oldSettings := &oldSettingsModel{Value: map[string]any{}}
err := txApp.DB().Select().From("_params").Where(dbx.HashExp{"key": "settings"}).One(oldSettings)
if err != nil {
return nil, err
}
// try without decrypt
plainDecodeErr := json.Unmarshal(oldSettings.RawValue, &oldSettings.Value)
// failed, try to decrypt
if plainDecodeErr != nil {
encryptionKey := os.Getenv(txApp.EncryptionEnv())
// load without decryption has failed and there is no encryption key to use for decrypt
if encryptionKey == "" {
return nil, fmt.Errorf("invalid settings db data or missing encryption key %q", txApp.EncryptionEnv())
}
// decrypt
decrypted, decryptErr := security.Decrypt(string(oldSettings.RawValue), encryptionKey)
if decryptErr != nil {
return nil, decryptErr
}
// decode again
decryptedDecodeErr := json.Unmarshal(decrypted, &oldSettings.Value)
if decryptedDecodeErr != nil {
return nil, decryptedDecodeErr
}
}
return oldSettings, nil
}
func migrateSettings(txApp core.App, oldSettings *oldSettingsModel) error {
// renamed old params collection
_, err := txApp.DB().RenameTable("_params", "_params_old").Execute()
if err != nil {
return err
}
// create new params table
err = createParamsTable(txApp)
if err != nil {
return err
}
// migrate old settings
newSettings := txApp.Settings()
// ---
newSettings.Meta.AppName = cast.ToString(getMapVal(oldSettings.Value, "meta", "appName"))
newSettings.Meta.AppURL = strings.TrimSuffix(cast.ToString(getMapVal(oldSettings.Value, "meta", "appUrl")), "/")
newSettings.Meta.HideControls = cast.ToBool(getMapVal(oldSettings.Value, "meta", "hideControls"))
newSettings.Meta.SenderName = cast.ToString(getMapVal(oldSettings.Value, "meta", "senderName"))
newSettings.Meta.SenderAddress = cast.ToString(getMapVal(oldSettings.Value, "meta", "senderAddress"))
// ---
newSettings.Logs.MaxDays = cast.ToInt(getMapVal(oldSettings.Value, "logs", "maxDays"))
newSettings.Logs.MinLevel = cast.ToInt(getMapVal(oldSettings.Value, "logs", "minLevel"))
newSettings.Logs.LogIP = cast.ToBool(getMapVal(oldSettings.Value, "logs", "logIp"))
// ---
newSettings.SMTP.Enabled = cast.ToBool(getMapVal(oldSettings.Value, "smtp", "enabled"))
newSettings.SMTP.Port = cast.ToInt(getMapVal(oldSettings.Value, "smtp", "port"))
newSettings.SMTP.Host = cast.ToString(getMapVal(oldSettings.Value, "smtp", "host"))
newSettings.SMTP.Username = cast.ToString(getMapVal(oldSettings.Value, "smtp", "username"))
newSettings.SMTP.Password = cast.ToString(getMapVal(oldSettings.Value, "smtp", "password"))
newSettings.SMTP.AuthMethod = cast.ToString(getMapVal(oldSettings.Value, "smtp", "authMethod"))
newSettings.SMTP.TLS = cast.ToBool(getMapVal(oldSettings.Value, "smtp", "tls"))
newSettings.SMTP.LocalName = cast.ToString(getMapVal(oldSettings.Value, "smtp", "localName"))
// ---
newSettings.Backups.Cron = cast.ToString(getMapVal(oldSettings.Value, "backups", "cron"))
newSettings.Backups.CronMaxKeep = cast.ToInt(getMapVal(oldSettings.Value, "backups", "cronMaxKeep"))
newSettings.Backups.S3 = core.S3Config{
Enabled: cast.ToBool(getMapVal(oldSettings.Value, "backups", "s3", "enabled")),
Bucket: cast.ToString(getMapVal(oldSettings.Value, "backups", "s3", "bucket")),
Region: cast.ToString(getMapVal(oldSettings.Value, "backups", "s3", "region")),
Endpoint: cast.ToString(getMapVal(oldSettings.Value, "backups", "s3", "endpoint")),
AccessKey: cast.ToString(getMapVal(oldSettings.Value, "backups", "s3", "accessKey")),
Secret: cast.ToString(getMapVal(oldSettings.Value, "backups", "s3", "secret")),
ForcePathStyle: cast.ToBool(getMapVal(oldSettings.Value, "backups", "s3", "forcePathStyle")),
}
// ---
newSettings.S3 = core.S3Config{
Enabled: cast.ToBool(getMapVal(oldSettings.Value, "s3", "enabled")),
Bucket: cast.ToString(getMapVal(oldSettings.Value, "s3", "bucket")),
Region: cast.ToString(getMapVal(oldSettings.Value, "s3", "region")),
Endpoint: cast.ToString(getMapVal(oldSettings.Value, "s3", "endpoint")),
AccessKey: cast.ToString(getMapVal(oldSettings.Value, "s3", "accessKey")),
Secret: cast.ToString(getMapVal(oldSettings.Value, "s3", "secret")),
ForcePathStyle: cast.ToBool(getMapVal(oldSettings.Value, "s3", "forcePathStyle")),
}
// ---
err = txApp.Save(newSettings)
if err != nil {
return err
}
// remove old params table
_, err = txApp.DB().DropTable("_params_old").Execute()
if err != nil {
return err
}
return nil
}
// -------------------------------------------------------------------
func migrateExternalAuths(txApp core.App) error {
// renamed old externalAuths table
_, err := txApp.DB().RenameTable("_externalAuths", "_externalAuths_old").Execute()
if err != nil {
return err
}
// create new externalAuths collection and table
err = createExternalAuthsCollection(txApp)
if err != nil {
return err
}
// copy old externalAuths records into the new one
_, err = txApp.DB().NewQuery(`
INSERT INTO {{` + core.CollectionNameExternalAuths + `}} ([[id]], [[collectionRef]], [[recordRef]], [[provider]], [[providerId]], [[created]], [[updated]])
SELECT [[id]], [[collectionId]], [[recordId]], [[provider]], [[providerId]], [[created]], [[updated]] FROM {{_externalAuths_old}};
`).Execute()
if err != nil {
return err
}
// remove old externalAuths table
_, err = txApp.DB().DropTable("_externalAuths_old").Execute()
if err != nil {
return err
}
return nil
}
// -------------------------------------------------------------------
func migrateOldCollections(txApp core.App, oldSettings *oldSettingsModel) error {
oldCollections := []*OldCollectionModel{}
err := txApp.DB().Select().From("_collections").All(&oldCollections)
if err != nil {
return err
}
for _, c := range oldCollections {
dummyAuthCollection := core.NewAuthCollection("test")
options := c.Options
c.Options = types.JSONMap[any]{} // reset
// update rules
// ---
c.ListRule = migrateRule(c.ListRule)
c.ViewRule = migrateRule(c.ViewRule)
c.CreateRule = migrateRule(c.CreateRule)
c.UpdateRule = migrateRule(c.UpdateRule)
c.DeleteRule = migrateRule(c.DeleteRule)
// migrate fields
// ---
for i, field := range c.Schema {
switch cast.ToString(field["type"]) {
case "bool":
field = toBoolField(field)
case "number":
field = toNumberField(field)
case "text":
field = toTextField(field)
case "url":
field = toURLField(field)
case "email":
field = toEmailField(field)
case "editor":
field = toEditorField(field)
case "date":
field = toDateField(field)
case "select":
field = toSelectField(field)
case "json":
field = toJSONField(field)
case "relation":
field = toRelationField(field)
case "file":
field = toFileField(field)
}
c.Schema[i] = field
}
// type specific changes
switch c.Type {
case "auth":
// token configs
// ---
c.Options["authToken"] = map[string]any{
"secret": zeroFallback(cast.ToString(getMapVal(oldSettings.Value, "recordAuthToken", "secret")), dummyAuthCollection.AuthToken.Secret),
"duration": zeroFallback(cast.ToInt64(getMapVal(oldSettings.Value, "recordAuthToken", "duration")), dummyAuthCollection.AuthToken.Duration),
}
c.Options["passwordResetToken"] = map[string]any{
"secret": zeroFallback(cast.ToString(getMapVal(oldSettings.Value, "recordPasswordResetToken", "secret")), dummyAuthCollection.PasswordResetToken.Secret),
"duration": zeroFallback(cast.ToInt64(getMapVal(oldSettings.Value, "recordPasswordResetToken", "duration")), dummyAuthCollection.PasswordResetToken.Duration),
}
c.Options["emailChangeToken"] = map[string]any{
"secret": zeroFallback(cast.ToString(getMapVal(oldSettings.Value, "recordEmailChangeToken", "secret")), dummyAuthCollection.EmailChangeToken.Secret),
"duration": zeroFallback(cast.ToInt64(getMapVal(oldSettings.Value, "recordEmailChangeToken", "duration")), dummyAuthCollection.EmailChangeToken.Duration),
}
c.Options["verificationToken"] = map[string]any{
"secret": zeroFallback(cast.ToString(getMapVal(oldSettings.Value, "recordVerificationToken", "secret")), dummyAuthCollection.VerificationToken.Secret),
"duration": zeroFallback(cast.ToInt64(getMapVal(oldSettings.Value, "recordVerificationToken", "duration")), dummyAuthCollection.VerificationToken.Duration),
}
c.Options["fileToken"] = map[string]any{
"secret": zeroFallback(cast.ToString(getMapVal(oldSettings.Value, "recordFileToken", "secret")), dummyAuthCollection.FileToken.Secret),
"duration": zeroFallback(cast.ToInt64(getMapVal(oldSettings.Value, "recordFileToken", "duration")), dummyAuthCollection.FileToken.Duration),
}
onlyVerified := cast.ToBool(options["onlyVerified"])
if onlyVerified {
c.Options["authRule"] = "verified=true"
} else {
c.Options["authRule"] = ""
}
c.Options["manageRule"] = nil
if options["manageRule"] != nil {
manageRule := cast.ToString(options["manageRule"])
c.Options["manageRule"] = &manageRule
}
// passwordAuth
identityFields := []string{}
if cast.ToBool(options["allowEmailAuth"]) {
identityFields = append(identityFields, "email")
}
if cast.ToBool(options["allowUsernameAuth"]) {
identityFields = append(identityFields, "username")
}
c.Options["passwordAuth"] = map[string]any{
"enabled": len(identityFields) > 0,
"identityFields": identityFields,
}
// oauth2
// ---
oauth2Providers := []map[string]any{}
providerNames := []string{
"googleAuth",
"facebookAuth",
"githubAuth",
"gitlabAuth",
"discordAuth",
"twitterAuth",
"microsoftAuth",
"spotifyAuth",
"kakaoAuth",
"twitchAuth",
"stravaAuth",
"giteeAuth",
"livechatAuth",
"giteaAuth",
"oidcAuth",
"oidc2Auth",
"oidc3Auth",
"appleAuth",
"instagramAuth",
"vkAuth",
"yandexAuth",
"patreonAuth",
"mailcowAuth",
"bitbucketAuth",
"planningcenterAuth",
}
for _, name := range providerNames {
if !cast.ToBool(getMapVal(oldSettings.Value, name, "enabled")) {
continue
}
oauth2Providers = append(oauth2Providers, map[string]any{
"name": strings.TrimSuffix(name, "Auth"),
"clientId": cast.ToString(getMapVal(oldSettings.Value, name, "clientId")),
"clientSecret": cast.ToString(getMapVal(oldSettings.Value, name, "clientSecret")),
"authURL": cast.ToString(getMapVal(oldSettings.Value, name, "authUrl")),
"tokenURL": cast.ToString(getMapVal(oldSettings.Value, name, "tokenUrl")),
"userInfoURL": cast.ToString(getMapVal(oldSettings.Value, name, "userApiUrl")),
"displayName": cast.ToString(getMapVal(oldSettings.Value, name, "displayName")),
"pkce": getMapVal(oldSettings.Value, name, "pkce"),
})
}
c.Options["oauth2"] = map[string]any{
"enabled": cast.ToBool(options["allowOAuth2Auth"]) && len(oauth2Providers) > 0,
"providers": oauth2Providers,
"mappedFields": map[string]string{
"username": "username",
},
}
// default email templates
// ---
emailTemplates := map[string]core.EmailTemplate{
"verificationTemplate": dummyAuthCollection.VerificationTemplate,
"resetPasswordTemplate": dummyAuthCollection.ResetPasswordTemplate,
"confirmEmailChangeTemplate": dummyAuthCollection.ConfirmEmailChangeTemplate,
}
for name, fallback := range emailTemplates {
c.Options[name] = map[string]any{
"subject": zeroFallback(
cast.ToString(getMapVal(oldSettings.Value, "meta", name, "subject")),
fallback.Subject,
),
"body": zeroFallback(
strings.ReplaceAll(
cast.ToString(getMapVal(oldSettings.Value, "meta", name, "body")),
"{ACTION_URL}",
cast.ToString(getMapVal(oldSettings.Value, "meta", name, "actionUrl")),
),
fallback.Body,
),
}
}
// mfa
// ---
c.Options["mfa"] = map[string]any{
"enabled": dummyAuthCollection.MFA.Enabled,
"duration": dummyAuthCollection.MFA.Duration,
"rule": dummyAuthCollection.MFA.Rule,
}
// otp
// ---
c.Options["otp"] = map[string]any{
"enabled": dummyAuthCollection.OTP.Enabled,
"duration": dummyAuthCollection.OTP.Duration,
"length": dummyAuthCollection.OTP.Length,
"emailTemplate": map[string]any{
"subject": dummyAuthCollection.OTP.EmailTemplate.Subject,
"body": dummyAuthCollection.OTP.EmailTemplate.Body,
},
}
// auth alerts
// ---
c.Options["authAlert"] = map[string]any{
"enabled": dummyAuthCollection.AuthAlert.Enabled,
"emailTemplate": map[string]any{
"subject": dummyAuthCollection.AuthAlert.EmailTemplate.Subject,
"body": dummyAuthCollection.AuthAlert.EmailTemplate.Body,
},
}
// add system field indexes
// ---
c.Indexes = append(types.JSONArray[string]{
fmt.Sprintf("CREATE UNIQUE INDEX `_%s_username_idx` ON `%s` (username COLLATE NOCASE)", c.Id, c.Name),
fmt.Sprintf("CREATE UNIQUE INDEX `_%s_email_idx` ON `%s` (email) WHERE email != ''", c.Id, c.Name),
fmt.Sprintf("CREATE UNIQUE INDEX `_%s_tokenKey_idx` ON `%s` (tokenKey)", c.Id, c.Name),
}, c.Indexes...)
// prepend the auth system fields
// ---
tokenKeyField := map[string]any{
"type": "text",
"id": "_pbf_auth_tokenKey_",
"name": "tokenKey",
"system": true,
"hidden": true,
"required": true,
"presentable": false,
"primaryKey": false,
"min": 30,
"max": 60,
"pattern": "",
"autogeneratePattern": "[a-zA-Z0-9_]{50}",
}
passwordField := map[string]any{
"type": "password",
"id": "_pbf_auth_password_",
"name": "password",
"presentable": false,
"system": true,
"hidden": true,
"required": true,
"pattern": "",
"min": cast.ToInt(options["minPasswordLength"]),
"cost": bcrypt.DefaultCost, // new default
}
emailField := map[string]any{
"type": "email",
"id": "_pbf_auth_email_",
"name": "email",
"system": true,
"hidden": false,
"presentable": false,
"required": cast.ToBool(options["requireEmail"]),
"exceptDomains": cast.ToStringSlice(options["exceptEmailDomains"]),
"onlyDomains": cast.ToStringSlice(options["onlyEmailDomains"]),
}
emailVisibilityField := map[string]any{
"type": "bool",
"id": "_pbf_auth_emailVisibility_",
"name": "emailVisibility",
"system": true,
"hidden": false,
"presentable": false,
"required": false,
}
verifiedField := map[string]any{
"type": "bool",
"id": "_pbf_auth_verified_",
"name": "verified",
"system": true,
"hidden": false,
"presentable": false,
"required": false,
}
usernameField := map[string]any{
"type": "text",
"id": "_pbf_auth_username_",
"name": "username",
"system": false,
"hidden": false,
"required": true,
"presentable": false,
"primaryKey": false,
"min": 3,
"max": 150,
"pattern": `^[\w][\w\.\-]*$`,
"autogeneratePattern": "users[0-9]{6}",
}
c.Schema = append(types.JSONArray[types.JSONMap[any]]{
passwordField,
tokenKeyField,
emailField,
emailVisibilityField,
verifiedField,
usernameField,
}, c.Schema...)
// rename passwordHash records rable column to password
// ---
_, err = txApp.DB().RenameColumn(c.Name, "passwordHash", "password").Execute()
if err != nil {
return err
}
// delete unnecessary auth columns
dropColumns := []string{"lastResetSentAt", "lastVerificationSentAt", "lastAuthAlertSentAt"}
for _, drop := range dropColumns {
// ignore errors in case the columns don't exist
_, _ = txApp.DB().DropColumn(c.Name, drop).Execute()
}
case "view":
c.Options["viewQuery"] = cast.ToString(options["query"])
}
// prepend the id field
idField := map[string]any{
"type": "text",
"id": "_pbf_text_id_",
"name": "id",
"system": true,
"required": true,
"presentable": false,
"hidden": false,
"primaryKey": true,
"min": 15,
"max": 15,
"pattern": "^[a-z0-9]+$",
"autogeneratePattern": "[a-z0-9]{15}",
}
c.Schema = append(types.JSONArray[types.JSONMap[any]]{idField}, c.Schema...)
var addCreated, addUpdated bool
if c.Type == "view" {
// manually check if the view has created/updated columns
columns, _ := txApp.TableColumns(c.Name)
for _, c := range columns {
if strings.EqualFold(c, "created") {
addCreated = true
} else if strings.EqualFold(c, "updated") {
addUpdated = true
}
}
} else {
addCreated = true
addUpdated = true
}
if addCreated {
createdField := map[string]any{
"type": "autodate",
"id": "_pbf_autodate_created_",
"name": "created",
"system": false,
"presentable": false,
"hidden": false,
"onCreate": true,
"onUpdate": false,
}
c.Schema = append(c.Schema, createdField)
}
if addUpdated {
updatedField := map[string]any{
"type": "autodate",
"id": "_pbf_autodate_updated_",
"name": "updated",
"system": false,
"presentable": false,
"hidden": false,
"onCreate": true,
"onUpdate": true,
}
c.Schema = append(c.Schema, updatedField)
}
if err = txApp.DB().Model(c).Update(); err != nil {
return err
}
}
_, err = txApp.DB().RenameColumn("_collections", "schema", "fields").Execute()
if err != nil {
return err
}
// run collection validations
collections, err := txApp.FindAllCollections()
if err != nil {
return fmt.Errorf("failed to retrieve all collections: %w", err)
}
for _, c := range collections {
err = txApp.Validate(c)
if err != nil {
return fmt.Errorf("migrated collection %q validation failure: %w", c.Name, err)
}
}
return nil
}
type OldCollectionModel struct {
Id string `db:"id" json:"id"`
Created types.DateTime `db:"created" json:"created"`
Updated types.DateTime `db:"updated" json:"updated"`
Name string `db:"name" json:"name"`
Type string `db:"type" json:"type"`
System bool `db:"system" json:"system"`
Schema types.JSONArray[types.JSONMap[any]] `db:"schema" json:"schema"`
Indexes types.JSONArray[string] `db:"indexes" json:"indexes"`
ListRule *string `db:"listRule" json:"listRule"`
ViewRule *string `db:"viewRule" json:"viewRule"`
CreateRule *string `db:"createRule" json:"createRule"`
UpdateRule *string `db:"updateRule" json:"updateRule"`
DeleteRule *string `db:"deleteRule" json:"deleteRule"`
Options types.JSONMap[any] `db:"options" json:"options"`
}
func (c OldCollectionModel) TableName() string {
return "_collections"
}
func migrateRule(rule *string) *string {
if rule == nil {
return nil
}
str := strings.ReplaceAll(*rule, "@request.data", "@request.body")
return &str
}
func toBoolField(data map[string]any) map[string]any {
return map[string]any{
"type": "bool",
"id": cast.ToString(data["id"]),
"name": cast.ToString(data["name"]),
"system": cast.ToBool(data["system"]),
"required": cast.ToBool(data["required"]),
"presentable": cast.ToBool(data["presentable"]),
"hidden": false,
}
}
func toNumberField(data map[string]any) map[string]any {
return map[string]any{
"type": "number",
"id": cast.ToString(data["id"]),
"name": cast.ToString(data["name"]),
"system": cast.ToBool(data["system"]),
"required": cast.ToBool(data["required"]),
"presentable": cast.ToBool(data["presentable"]),
"hidden": false,
"onlyInt": cast.ToBool(getMapVal(data, "options", "noDecimal")),
"min": getMapVal(data, "options", "min"),
"max": getMapVal(data, "options", "max"),
}
}
func toTextField(data map[string]any) map[string]any {
return map[string]any{
"type": "text",
"id": cast.ToString(data["id"]),
"name": cast.ToString(data["name"]),
"system": cast.ToBool(data["system"]),
"primaryKey": cast.ToBool(data["primaryKey"]),
"hidden": cast.ToBool(data["hidden"]),
"presentable": cast.ToBool(data["presentable"]),
"required": cast.ToBool(data["required"]),
"min": cast.ToInt(getMapVal(data, "options", "min")),
"max": cast.ToInt(getMapVal(data, "options", "max")),
"pattern": cast.ToString(getMapVal(data, "options", "pattern")),
"autogeneratePattern": cast.ToString(getMapVal(data, "options", "autogeneratePattern")),
}
}
func toEmailField(data map[string]any) map[string]any {
return map[string]any{
"type": "email",
"id": cast.ToString(data["id"]),
"name": cast.ToString(data["name"]),
"system": cast.ToBool(data["system"]),
"required": cast.ToBool(data["required"]),
"presentable": cast.ToBool(data["presentable"]),
"hidden": false,
"exceptDomains": cast.ToStringSlice(getMapVal(data, "options", "exceptDomains")),
"onlyDomains": cast.ToStringSlice(getMapVal(data, "options", "onlyDomains")),
}
}
func toURLField(data map[string]any) map[string]any {
return map[string]any{
"type": "url",
"id": cast.ToString(data["id"]),
"name": cast.ToString(data["name"]),
"system": cast.ToBool(data["system"]),
"required": cast.ToBool(data["required"]),
"presentable": cast.ToBool(data["presentable"]),
"hidden": false,
"exceptDomains": cast.ToStringSlice(getMapVal(data, "options", "exceptDomains")),
"onlyDomains": cast.ToStringSlice(getMapVal(data, "options", "onlyDomains")),
}
}
func toEditorField(data map[string]any) map[string]any {
return map[string]any{
"type": "editor",
"id": cast.ToString(data["id"]),
"name": cast.ToString(data["name"]),
"system": cast.ToBool(data["system"]),
"required": cast.ToBool(data["required"]),
"presentable": cast.ToBool(data["presentable"]),
"hidden": false,
"convertURLs": cast.ToBool(getMapVal(data, "options", "convertUrls")),
}
}
func toDateField(data map[string]any) map[string]any {
return map[string]any{
"type": "date",
"id": cast.ToString(data["id"]),
"name": cast.ToString(data["name"]),
"system": cast.ToBool(data["system"]),
"required": cast.ToBool(data["required"]),
"presentable": cast.ToBool(data["presentable"]),
"hidden": false,
"min": cast.ToString(getMapVal(data, "options", "min")),
"max": cast.ToString(getMapVal(data, "options", "max")),
}
}
func toJSONField(data map[string]any) map[string]any {
return map[string]any{
"type": "json",
"id": cast.ToString(data["id"]),
"name": cast.ToString(data["name"]),
"system": cast.ToBool(data["system"]),
"required": cast.ToBool(data["required"]),
"presentable": cast.ToBool(data["presentable"]),
"hidden": false,
"maxSize": cast.ToInt64(getMapVal(data, "options", "maxSize")),
}
}
func toSelectField(data map[string]any) map[string]any {
return map[string]any{
"type": "select",
"id": cast.ToString(data["id"]),
"name": cast.ToString(data["name"]),
"system": cast.ToBool(data["system"]),
"required": cast.ToBool(data["required"]),
"presentable": cast.ToBool(data["presentable"]),
"hidden": false,
"values": cast.ToStringSlice(getMapVal(data, "options", "values")),
"maxSelect": cast.ToInt(getMapVal(data, "options", "maxSelect")),
}
}
func toRelationField(data map[string]any) map[string]any {
maxSelect := cast.ToInt(getMapVal(data, "options", "maxSelect"))
if maxSelect <= 0 {
maxSelect = 2147483647
}
return map[string]any{
"type": "relation",
"id": cast.ToString(data["id"]),
"name": cast.ToString(data["name"]),
"system": cast.ToBool(data["system"]),
"required": cast.ToBool(data["required"]),
"presentable": cast.ToBool(data["presentable"]),
"hidden": false,
"collectionId": cast.ToString(getMapVal(data, "options", "collectionId")),
"cascadeDelete": cast.ToBool(getMapVal(data, "options", "cascadeDelete")),
"minSelect": cast.ToInt(getMapVal(data, "options", "minSelect")),
"maxSelect": maxSelect,
}
}
func toFileField(data map[string]any) map[string]any {
return map[string]any{
"type": "file",
"id": cast.ToString(data["id"]),
"name": cast.ToString(data["name"]),
"system": cast.ToBool(data["system"]),
"required": cast.ToBool(data["required"]),
"presentable": cast.ToBool(data["presentable"]),
"hidden": false,
"maxSelect": cast.ToInt(getMapVal(data, "options", "maxSelect")),
"maxSize": cast.ToInt64(getMapVal(data, "options", "maxSize")),
"thumbs": cast.ToStringSlice(getMapVal(data, "options", "thumbs")),
"mimeTypes": cast.ToStringSlice(getMapVal(data, "options", "mimeTypes")),
"protected": cast.ToBool(getMapVal(data, "options", "protected")),
}
}
func getMapVal(m map[string]any, keys ...string) any {
if len(keys) == 0 {
return nil
}
result, ok := m[keys[0]]
if !ok {
return nil
}
// end key reached
if len(keys) == 1 {
return result
}
if m, ok = result.(map[string]any); !ok {
return nil
}
return getMapVal(m, keys[1:]...)
}
func zeroFallback[T comparable](v T, fallback T) T {
var zero T
if v == zero {
return fallback
}
return v
}
@@ -1,56 +0,0 @@
package migrations
import (
"slices"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/tools/security"
)
// adds a "lastLoginAlertSentAt" column to all auth collection tables (if not already)
func init() {
AppMigrations.Register(func(db dbx.Builder) error {
dao := daos.New(db)
collections := []*models.Collection{}
err := dao.CollectionQuery().AndWhere(dbx.HashExp{"type": models.CollectionTypeAuth}).All(&collections)
if err != nil {
return err
}
var needToResetTokens bool
for _, c := range collections {
columns, err := dao.TableColumns(c.Name)
if err != nil {
return err
}
if slices.Contains(columns, schema.FieldNameLastLoginAlertSentAt) {
continue // already inserted
}
_, err = db.AddColumn(c.Name, schema.FieldNameLastLoginAlertSentAt, "TEXT DEFAULT '' NOT NULL").Execute()
if err != nil {
return err
}
opts := c.AuthOptions()
if opts.AllowOAuth2Auth && (opts.AllowEmailAuth || opts.AllowUsernameAuth) {
needToResetTokens = true
}
}
settings, _ := dao.FindSettings()
if needToResetTokens && settings != nil {
settings.RecordAuthToken.Secret = security.RandomString(50)
if err := dao.SaveSettings(settings); err != nil {
return err
}
}
return nil
}, nil)
}
-38
View File
@@ -1,38 +0,0 @@
package logs
import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/tools/migrate"
)
var LogsMigrations migrate.MigrationsList
func init() {
LogsMigrations.Register(func(db dbx.Builder) error {
_, err := db.NewQuery(`
CREATE TABLE {{_requests}} (
[[id]] TEXT PRIMARY KEY NOT NULL,
[[url]] TEXT DEFAULT "" NOT NULL,
[[method]] TEXT DEFAULT "get" NOT NULL,
[[status]] INTEGER DEFAULT 200 NOT NULL,
[[auth]] TEXT DEFAULT "guest" NOT NULL,
[[ip]] TEXT DEFAULT "127.0.0.1" NOT NULL,
[[referer]] TEXT DEFAULT "" NOT NULL,
[[userAgent]] TEXT DEFAULT "" NOT NULL,
[[meta]] JSON DEFAULT "{}" NOT NULL,
[[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
[[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL
);
CREATE INDEX _request_status_idx on {{_requests}} ([[status]]);
CREATE INDEX _request_auth_idx on {{_requests}} ([[auth]]);
CREATE INDEX _request_ip_idx on {{_requests}} ([[ip]]);
CREATE INDEX _request_created_hour_idx on {{_requests}} (strftime('%Y-%m-%d %H:00:00', [[created]]));
`).Execute()
return err
}, func(db dbx.Builder) error {
_, err := db.DropTable("_requests").Execute()
return err
})
}
@@ -1,57 +0,0 @@
package logs
import (
"github.com/pocketbase/dbx"
)
func init() {
LogsMigrations.Register(func(db dbx.Builder) error {
// delete old index (don't check for error because of backward compatibility with old installations)
db.DropIndex("_requests", "_request_ip_idx").Execute()
// rename ip -> remoteIp
if _, err := db.RenameColumn("_requests", "ip", "remoteIp").Execute(); err != nil {
return err
}
// add new userIp column
if _, err := db.AddColumn("_requests", "userIp", `TEXT DEFAULT "127.0.0.1" NOT NULL`).Execute(); err != nil {
return err
}
// add new indexes
if _, err := db.CreateIndex("_requests", "_request_remote_ip_idx", "remoteIp").Execute(); err != nil {
return err
}
if _, err := db.CreateIndex("_requests", "_request_user_ip_idx", "userIp").Execute(); err != nil {
return err
}
return nil
}, func(db dbx.Builder) error {
// delete new indexes
if _, err := db.DropIndex("_requests", "_request_remote_ip_idx").Execute(); err != nil {
return err
}
if _, err := db.DropIndex("_requests", "_request_user_ip_idx").Execute(); err != nil {
return err
}
// drop userIp column
if _, err := db.DropColumn("_requests", "userIp").Execute(); err != nil {
return err
}
// restore original remoteIp column name
if _, err := db.RenameColumn("_requests", "remoteIp", "ip").Execute(); err != nil {
return err
}
// restore original index
if _, err := db.CreateIndex("_requests", "_request_ip_idx", "ip").Execute(); err != nil {
return err
}
return nil
})
}
@@ -1,18 +0,0 @@
package logs
import (
"github.com/pocketbase/dbx"
)
// This migration normalizes the request logs method to UPPERCASE (eg. "get" => "GET").
func init() {
LogsMigrations.Register(func(db dbx.Builder) error {
_, err := db.NewQuery("UPDATE {{_requests}} SET method=UPPER(method)").Execute()
return err
}, func(db dbx.Builder) error {
_, err := db.NewQuery("UPDATE {{_requests}} SET method=LOWER(method)").Execute()
return err
})
}
@@ -1,57 +0,0 @@
package logs
import (
"github.com/pocketbase/dbx"
)
func init() {
LogsMigrations.Register(func(db dbx.Builder) error {
if _, err := db.DropTable("_requests").Execute(); err != nil {
return err
}
_, err := db.NewQuery(`
CREATE TABLE {{_logs}} (
[[id]] TEXT PRIMARY KEY DEFAULT ('r'||lower(hex(randomblob(7)))) NOT NULL,
[[level]] INTEGER DEFAULT 0 NOT NULL,
[[message]] TEXT DEFAULT "" NOT NULL,
[[data]] JSON DEFAULT "{}" NOT NULL,
[[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
[[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL
);
CREATE INDEX _logs_level_idx on {{_logs}} ([[level]]);
CREATE INDEX _logs_message_idx on {{_logs}} ([[message]]);
CREATE INDEX _logs_created_hour_idx on {{_logs}} (strftime('%Y-%m-%d %H:00:00', [[created]]));
`).Execute()
return err
}, func(db dbx.Builder) error {
if _, err := db.DropTable("_logs").Execute(); err != nil {
return err
}
_, err := db.NewQuery(`
CREATE TABLE {{_requests}} (
[[id]] TEXT PRIMARY KEY NOT NULL,
[[url]] TEXT DEFAULT "" NOT NULL,
[[method]] TEXT DEFAULT "get" NOT NULL,
[[status]] INTEGER DEFAULT 200 NOT NULL,
[[auth]] TEXT DEFAULT "guest" NOT NULL,
[[ip]] TEXT DEFAULT "127.0.0.1" NOT NULL,
[[referer]] TEXT DEFAULT "" NOT NULL,
[[userAgent]] TEXT DEFAULT "" NOT NULL,
[[meta]] JSON DEFAULT "{}" NOT NULL,
[[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
[[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL
);
CREATE INDEX _request_status_idx on {{_requests}} ([[status]]);
CREATE INDEX _request_auth_idx on {{_requests}} ([[auth]]);
CREATE INDEX _request_ip_idx on {{_requests}} ([[ip]]);
CREATE INDEX _request_created_hour_idx on {{_requests}} (strftime('%Y-%m-%d %H:00:00', [[created]]));
`).Execute()
return err
})
}