merge v0.23.0-rc changes
This commit is contained in:
+301
-110
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user