diff --git a/README.md b/README.md
index 8d090cbc1..b844976be 100644
--- a/README.md
+++ b/README.md
@@ -130,9 +130,13 @@ The following libraries and frameworks are used by GoToSocial, with gratitude
* [go-fed/activity](https://github.com/go-fed/activity); Golang ActivityPub/ActivityStreams library. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).
* [go-fed/httpsig](https://github.com/go-fed/httpsig); secure HTTP signature library. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).
* [google/uuid](https://github.com/google/uuid); UUID generation. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html)
+* [go-playground/validator](https://github.com/go-playground/validator); struct validation. [MIT License](https://spdx.org/licenses/MIT.html)
* [gorilla/websocket](https://github.com/gorilla/websocket); Websocket connectivity. [BSD-2-Clause License](https://spdx.org/licenses/BSD-2-Clause.html).
* [h2non/filetype](https://github.com/h2non/filetype); filetype checking. [MIT License](https://spdx.org/licenses/MIT.html).
* [jackc/pgx](https://github.com/jackc/pgx); Postgres driver. [MIT License](https://spdx.org/licenses/MIT.html).
+* [modernc.org/sqlite](https://gitlab.com/cznic/sqlite); cgo-free port of SQLite. [Other License](https://gitlab.com/cznic/sqlite/-/blob/master/LICENSE).
+ * [modernc.org/ccgo](https://gitlab.com/cznic/ccgo); c99 AST -> Go translater. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).
+ * [modernc.org/libc](https://gitlab.com/cznic/libc); C-runtime services. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).
* [microcosm-cc/bluemonday](https://github.com/microcosm-cc/bluemonday); HTML user-input sanitization. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).
* [mvdan/xurls](https://github.com/mvdan/xurls); URL parsing regular expressions. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).
* [nfnt/resize](https://github.com/nfnt/resize); convenient image resizing. [ISC License](https://spdx.org/licenses/ISC.html).
@@ -146,11 +150,9 @@ The following libraries and frameworks are used by GoToSocial, with gratitude
* [go-swagger/go-swagger](https://github.com/go-swagger/go-swagger); Swagger OpenAPI spec generation. [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html).
* [tdewolff/minify](https://github.com/tdewolff/minify); HTML minification. [MIT License](https://spdx.org/licenses/MIT.html).
* [uptrace/bun](https://github.com/uptrace/bun); database ORM. [BSD-2-Clause License](https://spdx.org/licenses/BSD-2-Clause.html).
+ *
* [urfave/cli](https://github.com/urfave/cli); command-line interface framework. [MIT License](https://spdx.org/licenses/MIT.html).
* [wagslane/go-password-validator](https://github.com/wagslane/go-password-validator); password strength validation. [MIT License](https://spdx.org/licenses/MIT.html).
-* [modernc.org/sqlite](sqlite); cgo-free port of SQLite. [Other License](https://gitlab.com/cznic/sqlite/-/blob/master/LICENSE).
- * [modernc.org/ccgo](ccgo); c99 AST -> Go translater. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).
- * [modernc.org/libc](libc); C-runtime services. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).
### Image Attribution
diff --git a/go.mod b/go.mod
index 9e0ef9a08..8cdd6baf7 100644
--- a/go.mod
+++ b/go.mod
@@ -21,7 +21,7 @@ require (
github.com/go-errors/errors v1.4.0 // indirect
github.com/go-fed/activity v1.0.1-0.20210803212804-d866ba75dd0f
github.com/go-fed/httpsig v1.1.0
- github.com/go-playground/validator/v10 v10.7.0 // indirect
+ github.com/go-playground/validator/v10 v10.7.0
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go
index 248232fe3..ee26eeee1 100644
--- a/internal/db/bundb/bundb.go
+++ b/internal/db/bundb/bundb.go
@@ -37,11 +37,13 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/cache"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/dialect/sqlitedialect"
+ "github.com/uptrace/bun/migrate"
_ "modernc.org/sqlite"
)
@@ -73,6 +75,29 @@ type bunDBService struct {
conn *DBConn
}
+func doMigration(ctx context.Context, db *bun.DB, log *logrus.Logger) error {
+ l := log.WithField("func", "doMigration")
+
+ migrator := migrate.NewMigrator(db, migrations.Migrations)
+
+ if err := migrator.Init(ctx); err != nil {
+ return err
+ }
+
+ group, err := migrator.Migrate(ctx)
+ if err != nil {
+ return err
+ }
+
+ if group.ID == 0 {
+ l.Info("there are no new migrations to run")
+ return nil
+ }
+
+ l.Infof("MIGRATED DATABASE TO %s", group)
+ return nil
+}
+
// NewBunDBService returns a bunDB derived from the provided config, which implements the go-fed DB interface.
// Under the hood, it uses https://github.com/uptrace/bun to create and maintain a database connection.
func NewBunDBService(ctx context.Context, c *config.Config, log *logrus.Logger) (db.DB, error) {
@@ -121,6 +146,10 @@ func NewBunDBService(ctx context.Context, c *config.Config, log *logrus.Logger)
conn.RegisterModel(t)
}
+ if err := doMigration(ctx, conn.DB, log); err != nil {
+ return nil, fmt.Errorf("db migration error: %s", err)
+ }
+
ps := &bunDBService{
Account: &accountDB{
config: c,
diff --git a/internal/db/bundb/migrations/20210816411877_struct_validation.go b/internal/db/bundb/migrations/20210816411877_struct_validation.go
new file mode 100644
index 000000000..660d94517
--- /dev/null
+++ b/internal/db/bundb/migrations/20210816411877_struct_validation.go
@@ -0,0 +1,54 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package migrations
+
+import (
+ "context"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/uptrace/bun"
+)
+
+func init() {
+ up := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ _, err := tx.NewCreateTable().Model(>smodel.Account{}).IfNotExists().Exec(ctx)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ }
+
+ down := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ _, err := tx.NewDropTable().Model(>smodel.Account{}).Exec(ctx)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ }
+
+ if err := Migrations.Register(up, down); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/db/bundb/migrations/README.md b/internal/db/bundb/migrations/README.md
new file mode 100644
index 000000000..e293d43c7
--- /dev/null
+++ b/internal/db/bundb/migrations/README.md
@@ -0,0 +1,21 @@
+# Migrations
+
+## How do I write a migration file?
+
+[See here](https://bun.uptrace.dev/guide/migrations.html#migration-names)
+
+As a template, take one of the existing migration files and modify it. It will be automatically loaded and handled by Bun.
+
+## File format
+
+Bun requires a very specific format: 14 digits, then letters or underscores.
+
+You can use the following bash command on your branch to generate a suitable migration filename.
+
+```bash
+echo "$(date --utc +%Y%m%H%M%S%N | head -c 14)_$(git rev-parse --abbrev-ref HEAD).go"
+```
+
+## Rules of thumb
+
+1. **DON'T DROP TABLES**!!!!!!!!
diff --git a/internal/db/bundb/migrations/main.go b/internal/db/bundb/migrations/main.go
new file mode 100644
index 000000000..7f4e76027
--- /dev/null
+++ b/internal/db/bundb/migrations/main.go
@@ -0,0 +1,28 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package migrations
+
+import (
+ "github.com/uptrace/bun/migrate"
+)
+
+var (
+ // Migrations provides migration logic for bun
+ Migrations = migrate.NewMigrations()
+)
diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go
index a59746be5..75ea02b4f 100644
--- a/internal/gtsmodel/account.go
+++ b/internal/gtsmodel/account.go
@@ -27,124 +27,56 @@ import (
"time"
)
-// Account represents either a local or a remote fediverse account, gotosocial or otherwise (mastodon, pleroma, etc)
+// Account represents either a local or a remote fediverse account, gotosocial or otherwise (mastodon, pleroma, etc).
type Account struct {
- /*
- BASIC INFO
- */
-
- // id of this account in the local database
- ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
- // Username of the account, should just be a string of [a-z0-9_]. Can be added to domain to create the full username in the form ``[username]@[domain]`` eg., ``user_96@example.org``
- Username string `bun:",notnull,unique:userdomain,nullzero"` // username and domain should be unique *with* each other
- // Domain of the account, will be null if this is a local account, otherwise something like ``example.org`` or ``mastodon.social``. Should be unique with username.
- Domain string `bun:",unique:userdomain,nullzero"` // username and domain should be unique *with* each other
-
- /*
- ACCOUNT METADATA
- */
-
- // ID of the avatar as a media attachment
- AvatarMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
- AvatarMediaAttachment *MediaAttachment `bun:"rel:belongs-to"`
- // For a non-local account, where can the header be fetched?
- AvatarRemoteURL string `bun:",nullzero"`
- // ID of the header as a media attachment
- HeaderMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
- HeaderMediaAttachment *MediaAttachment `bun:"rel:belongs-to"`
- // For a non-local account, where can the header be fetched?
- HeaderRemoteURL string `bun:",nullzero"`
- // DisplayName for this account. Can be empty, then just the Username will be used for display purposes.
- DisplayName string `bun:",nullzero"`
- // a key/value map of fields that this account has added to their profile
- Fields []Field
- // A note that this account has on their profile (ie., the account's bio/description of themselves)
- Note string `bun:",nullzero"`
- // Is this a memorial account, ie., has the user passed away?
- Memorial bool `bun:",nullzero"`
- // This account has moved this account id in the database
- MovedToAccountID string `bun:"type:CHAR(26),nullzero"`
- // When was this account created?
- CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
- // When was this account last updated?
- UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
- // Does this account identify itself as a bot?
- Bot bool
- // What reason was given for signing up when this account was created?
- Reason string `bun:",nullzero"`
-
- /*
- USER AND PRIVACY PREFERENCES
- */
-
- // Does this account need an approval for new followers?
- Locked bool `bun:",default:true"`
- // Should this account be shown in the instance's profile directory?
- Discoverable bool `bun:",default:false"`
- // Default post privacy for this account
- Privacy Visibility `bun:",default:'public'"`
- // Set posts from this account to sensitive by default?
- Sensitive bool `bun:",default:false"`
- // What language does this account post in?
- Language string `bun:",default:'en'"`
-
- /*
- ACTIVITYPUB THINGS
- */
-
- // What is the activitypub URI for this account discovered by webfinger?
- URI string `bun:",unique,nullzero"`
- // At which URL can we see the user account in a web browser?
- URL string `bun:",unique,nullzero"`
- // Last time this account was located using the webfinger API.
- LastWebfingeredAt time.Time `bun:",nullzero"`
- // Address of this account's activitypub inbox, for sending activity to
- InboxURI string `bun:",unique,nullzero"`
- // Address of this account's activitypub outbox
- OutboxURI string `bun:",unique,nullzero"`
- // URI for getting the following list of this account
- FollowingURI string `bun:",unique,nullzero"`
- // URI for getting the followers list of this account
- FollowersURI string `bun:",unique,nullzero"`
- // URL for getting the featured collection list of this account
- FeaturedCollectionURI string `bun:",unique,nullzero"`
- // What type of activitypub actor is this account?
- ActorType string `bun:",nullzero"`
- // This account is associated with x account id
- AlsoKnownAs string `bun:",nullzero"`
-
- /*
- CRYPTO FIELDS
- */
-
- // Privatekey for validating activitypub requests, will only be defined for local accounts
- PrivateKey *rsa.PrivateKey
- // Publickey for encoding activitypub requests, will be defined for both local and remote accounts
- PublicKey *rsa.PublicKey
- // Web-reachable location of this account's public key
- PublicKeyURI string `bun:",nullzero"`
-
- /*
- ADMIN FIELDS
- */
-
- // When was this account set to have all its media shown as sensitive?
- SensitizedAt time.Time `bun:",nullzero"`
- // When was this account silenced (eg., statuses only visible to followers, not public)?
- SilencedAt time.Time `bun:",nullzero"`
- // When was this account suspended (eg., don't allow it to log in/post, don't accept media/posts from this account)
- SuspendedAt time.Time `bun:",nullzero"`
- // Should we hide this account's collections?
- HideCollections bool
- // id of the database entry that caused this account to become suspended -- can be an account ID or a domain block ID
- SuspensionOrigin string `bun:"type:CHAR(26),nullzero"`
+ ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
+ CreatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item created
+ UpdatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item last updated
+ Username string `validate:"required" bun:",nullzero,notnull,unique:userdomain"` // Username of the account, should just be a string of [a-zA-Z0-9_]. Can be added to domain to create the full username in the form ``[username]@[domain]`` eg., ``user_96@example.org``. Username and domain should be unique *with* each other
+ Domain string `validate:"omitempty,fqdn" bun:",nullzero,unique:userdomain"` // Domain of the account, will be null if this is a local account, otherwise something like ``example.org`` or ``mastodon.social``. Should be unique with username.
+ AvatarMediaAttachmentID string `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"` // Database ID of the media attachment, if present
+ AvatarMediaAttachment *MediaAttachment `validate:"-" bun:"rel:belongs-to"` // MediaAttachment corresponding to avatarMediaAttachmentID
+ AvatarRemoteURL string `validate:"omitempty,url" bun:",nullzero"` // For a non-local account, where can the header be fetched?
+ HeaderMediaAttachmentID string `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"` // Database ID of the media attachment, if present
+ HeaderMediaAttachment *MediaAttachment `validate:"-" bun:"rel:belongs-to"` // MediaAttachment corresponding to headerMediaAttachmentID
+ HeaderRemoteURL string `validate:"omitempty,url" bun:",nullzero"` // For a non-local account, where can the header be fetched?
+ DisplayName string `validate:"-" bun:",nullzero"` // DisplayName for this account. Can be empty, then just the Username will be used for display purposes.
+ Fields []Field `validate:"-"` // a key/value map of fields that this account has added to their profile
+ Note string `validate:"-" bun:",nullzero"` // A note that this account has on their profile (ie., the account's bio/description of themselves)
+ Memorial bool `validate:"-" bun:",nullzero,default:false"` // Is this a memorial account, ie., has the user passed away?
+ AlsoKnownAs string `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"` // This account is associated with x account id
+ MovedToAccountID string `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"` // This account has moved this account id in the database
+ Bot bool `validate:"-" bun:",nullzero,default:false"` // Does this account identify itself as a bot?
+ Reason string `validate:"-" bun:",nullzero"` // What reason was given for signing up when this account was created?
+ Locked bool `validate:"-" bun:",nullzero,default:true"` // Does this account need an approval for new followers?
+ Discoverable bool `validate:"-" bun:",nullzero,default:false"` // Should this account be shown in the instance's profile directory?
+ Privacy Visibility `validate:"oneof=public unlocked followers_only mutuals_only direct" bun:",nullzero,notnull,default:'public'"` // Default post privacy for this account
+ Sensitive bool `validate:"-" bun:",nullzero,default:false"` // Set posts from this account to sensitive by default?
+ Language string `validate:"-" bun:",nullzero,notnull,default:'en'"` // What language does this account post in?
+ URI string `validate:"required,url" bun:",nullzero,notnull,unique"` // ActivityPub URI for this account.
+ URL string `validate:"omitempty,url" bun:",unique,nullzero"` // Web URL for this account's profile
+ LastWebfingeredAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // Last time this account was refreshed/located with webfinger.
+ InboxURI string `validate:"omitempty,url" bun:",nullzero,unique"` // Address of this account's ActivityPub inbox, for sending activity to
+ OutboxURI string `validate:"omitempty,url" bun:",nullzero,unique"` // Address of this account's activitypub outbox
+ FollowingURI string `validate:"omitempty,url" bun:",nullzero,unique"` // URI for getting the following list of this account
+ FollowersURI string `validate:"omitempty,url" bun:",nullzero,unique"` // URI for getting the followers list of this account
+ FeaturedCollectionURI string `validate:"omitempty,url" bun:",nullzero,unique"` // URL for getting the featured collection list of this account
+ ActorType string `validate:"oneof=Application Group Organization Person Service " bun:",nullzero,notnull"` // What type of activitypub actor is this account?
+ PrivateKey *rsa.PrivateKey `validate:"required_without=Domain"` // Privatekey for validating activitypub requests, will only be defined for local accounts
+ PublicKey *rsa.PublicKey `validate:"required"` // Publickey for encoding activitypub requests, will be defined for both local and remote accounts
+ PublicKeyURI string `validate:"required" bun:",nullzero,notnull"` // Web-reachable location of this account's public key
+ SensitizedAt time.Time `validate:"-" bun:",nullzero"` // When was this account set to have all its media shown as sensitive?
+ SilencedAt time.Time `validate:"-" bun:",nullzero"` // When was this account silenced (eg., statuses only visible to followers, not public)?
+ SuspendedAt time.Time `validate:"-" bun:",nullzero"` // When was this account suspended (eg., don't allow it to log in/post, don't accept media/posts from this account)
+ HideCollections bool `validate:"-" bun:",nullzero,default:false"` // Hide this account's collections
+ SuspensionOrigin string `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"` // id of the database entry that caused this account to become suspended -- can be an account ID or a domain block ID
}
// Field represents a key value field on an account, for things like pronouns, website, etc.
// VerifiedAt is optional, to be used only if Value is a URL to a webpage that contains the
// username of the user.
type Field struct {
- Name string
- Value string
- VerifiedAt time.Time `bun:",nullzero"`
+ Name string `validate:"required"` // Name of this field.
+ Value string `validate:"required"` // Value of this field.
+ VerifiedAt time.Time `validate:"-" bun:",nullzero"` // This field was verified at (optional).
}
diff --git a/internal/gtsmodel/block.go b/internal/gtsmodel/block.go
index 989dd8193..61595c12d 100644
--- a/internal/gtsmodel/block.go
+++ b/internal/gtsmodel/block.go
@@ -4,12 +4,12 @@ import "time"
// Block refers to the blocking of one account by another.
type Block struct {
- ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
- CreatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item created
- UpdatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item last updated
- URI string `validate:"required,url" bun:",notnull,nullzero,unique"` // ActivityPub uri of this block.
- AccountID string `validate:"required,ulid" bun:"type:CHAR(26),unique:frsrctarget,notnull"` // Who does this block originate from?
- Account *Account `validate:"-" bun:"rel:belongs-to"` // Account corresponding to accountID
- TargetAccountID string `validate:"required,ulid" bun:"type:CHAR(26),unique:frsrctarget,notnull"` // Who is the target of this block ?
- TargetAccount *Account `validate:"-" bun:"rel:belongs-to"` // Account corresponding to targetAccountID
+ ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
+ CreatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item created
+ UpdatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item last updated
+ URI string `validate:"required,url" bun:",notnull,nullzero,unique"` // ActivityPub uri of this block.
+ AccountID string `validate:"required,ulid" bun:"type:CHAR(26),unique:blocksrctarget,notnull"` // Who does this block originate from?
+ Account *Account `validate:"-" bun:"rel:belongs-to"` // Account corresponding to accountID
+ TargetAccountID string `validate:"required,ulid" bun:"type:CHAR(26),unique:blocksrctarget,notnull"` // Who is the target of this block ?
+ TargetAccount *Account `validate:"-" bun:"rel:belongs-to"` // Account corresponding to targetAccountID
}
diff --git a/internal/gtsmodel/block_test.go b/internal/gtsmodel/block_test.go
new file mode 100644
index 000000000..307f26cd9
--- /dev/null
+++ b/internal/gtsmodel/block_test.go
@@ -0,0 +1,115 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package gtsmodel_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func happyBlock() *gtsmodel.Block {
+ return >smodel.Block{
+ ID: "01FE91RJR88PSEEE30EV35QR8N",
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ URI: "https://example.org/accounts/someone/blocks/01FE91RJR88PSEEE30EV35QR8N",
+ AccountID: "01FEED79PRMVWPRMFHFQM8MJQN",
+ Account: nil,
+ TargetAccountID: "01FEEDMF6C0QD589MRK7919Z0R",
+ TargetAccount: nil,
+ }
+}
+
+type BlockValidateTestSuite struct {
+ suite.Suite
+}
+
+func (suite *BlockValidateTestSuite) TestValidateBlockHappyPath() {
+ // no problem here
+ d := happyBlock()
+ err := gtsmodel.ValidateStruct(*d)
+ suite.NoError(err)
+}
+
+func (suite *BlockValidateTestSuite) TestValidateBlockBadID() {
+ d := happyBlock()
+
+ d.ID = ""
+ err := gtsmodel.ValidateStruct(*d)
+ suite.EqualError(err, "Key: 'Block.ID' Error:Field validation for 'ID' failed on the 'required' tag")
+
+ d.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB"
+ err = gtsmodel.ValidateStruct(*d)
+ suite.EqualError(err, "Key: 'Block.ID' Error:Field validation for 'ID' failed on the 'ulid' tag")
+}
+
+func (suite *BlockValidateTestSuite) TestValidateBlockNoCreatedAt() {
+ d := happyBlock()
+
+ d.CreatedAt = time.Time{}
+ err := gtsmodel.ValidateStruct(*d)
+ suite.NoError(err)
+}
+
+func (suite *BlockValidateTestSuite) TestValidateBlockCreatedByAccountID() {
+ d := happyBlock()
+
+ d.AccountID = ""
+ err := gtsmodel.ValidateStruct(*d)
+ suite.EqualError(err, "Key: 'Block.AccountID' Error:Field validation for 'AccountID' failed on the 'required' tag")
+
+ d.AccountID = "this-is-not-a-valid-ulid"
+ err = gtsmodel.ValidateStruct(*d)
+ suite.EqualError(err, "Key: 'Block.AccountID' Error:Field validation for 'AccountID' failed on the 'ulid' tag")
+}
+
+func (suite *BlockValidateTestSuite) TestValidateBlockTargetAccountID() {
+ d := happyBlock()
+
+ d.TargetAccountID = "invalid-ulid"
+ err := gtsmodel.ValidateStruct(*d)
+ suite.EqualError(err, "Key: 'Block.TargetAccountID' Error:Field validation for 'TargetAccountID' failed on the 'ulid' tag")
+
+ d.TargetAccountID = "01FEEDHX4G7EGHF5GD9E82Y51Q"
+ err = gtsmodel.ValidateStruct(*d)
+ suite.NoError(err)
+
+ d.TargetAccountID = ""
+ err = gtsmodel.ValidateStruct(*d)
+ suite.EqualError(err, "Key: 'Block.TargetAccountID' Error:Field validation for 'TargetAccountID' failed on the 'required' tag")
+}
+
+func (suite *BlockValidateTestSuite) TestValidateBlockURI() {
+ d := happyBlock()
+
+ d.URI = "invalid-uri"
+ err := gtsmodel.ValidateStruct(*d)
+ suite.EqualError(err, "Key: 'Block.URI' Error:Field validation for 'URI' failed on the 'url' tag")
+
+ d.URI = ""
+ err = gtsmodel.ValidateStruct(*d)
+ suite.EqualError(err, "Key: 'Block.URI' Error:Field validation for 'URI' failed on the 'required' tag")
+}
+
+func TestBlockValidateTestSuite(t *testing.T) {
+ suite.Run(t, new(BlockValidateTestSuite))
+}
diff --git a/internal/gtsmodel/domainblock_test.go b/internal/gtsmodel/domainblock_test.go
new file mode 100644
index 000000000..ced29ab31
--- /dev/null
+++ b/internal/gtsmodel/domainblock_test.go
@@ -0,0 +1,121 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package gtsmodel_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func happyDomainBlock() *gtsmodel.DomainBlock {
+ return >smodel.DomainBlock{
+ ID: "01FE91RJR88PSEEE30EV35QR8N",
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ Domain: "baddudes.suck",
+ CreatedByAccountID: "01FEED79PRMVWPRMFHFQM8MJQN",
+ PrivateComment: "we don't like em",
+ PublicComment: "poo poo dudes",
+ Obfuscate: false,
+ SubscriptionID: "",
+ }
+}
+
+type DomainBlockValidateTestSuite struct {
+ suite.Suite
+}
+
+func (suite *DomainBlockValidateTestSuite) TestValidateDomainBlockHappyPath() {
+ // no problem here
+ d := happyDomainBlock()
+ err := gtsmodel.ValidateStruct(*d)
+ suite.NoError(err)
+}
+
+func (suite *DomainBlockValidateTestSuite) TestValidateDomainBlockBadID() {
+ d := happyDomainBlock()
+
+ d.ID = ""
+ err := gtsmodel.ValidateStruct(*d)
+ suite.EqualError(err, "Key: 'DomainBlock.ID' Error:Field validation for 'ID' failed on the 'required' tag")
+
+ d.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB"
+ err = gtsmodel.ValidateStruct(*d)
+ suite.EqualError(err, "Key: 'DomainBlock.ID' Error:Field validation for 'ID' failed on the 'ulid' tag")
+}
+
+func (suite *DomainBlockValidateTestSuite) TestValidateDomainBlockNoCreatedAt() {
+ d := happyDomainBlock()
+
+ d.CreatedAt = time.Time{}
+ err := gtsmodel.ValidateStruct(*d)
+ suite.NoError(err)
+}
+
+func (suite *DomainBlockValidateTestSuite) TestValidateDomainBlockBadDomain() {
+ d := happyDomainBlock()
+
+ d.Domain = ""
+ err := gtsmodel.ValidateStruct(*d)
+ suite.EqualError(err, "Key: 'DomainBlock.Domain' Error:Field validation for 'Domain' failed on the 'required' tag")
+
+ d.Domain = "this-is-not-a-valid-domain"
+ err = gtsmodel.ValidateStruct(*d)
+ suite.EqualError(err, "Key: 'DomainBlock.Domain' Error:Field validation for 'Domain' failed on the 'fqdn' tag")
+}
+
+func (suite *DomainBlockValidateTestSuite) TestValidateDomainBlockCreatedByAccountID() {
+ d := happyDomainBlock()
+
+ d.CreatedByAccountID = ""
+ err := gtsmodel.ValidateStruct(*d)
+ suite.EqualError(err, "Key: 'DomainBlock.CreatedByAccountID' Error:Field validation for 'CreatedByAccountID' failed on the 'required' tag")
+
+ d.CreatedByAccountID = "this-is-not-a-valid-ulid"
+ err = gtsmodel.ValidateStruct(*d)
+ suite.EqualError(err, "Key: 'DomainBlock.CreatedByAccountID' Error:Field validation for 'CreatedByAccountID' failed on the 'ulid' tag")
+}
+
+func (suite *DomainBlockValidateTestSuite) TestValidateDomainBlockComments() {
+ d := happyDomainBlock()
+
+ d.PrivateComment = ""
+ d.PublicComment = ""
+ err := gtsmodel.ValidateStruct(*d)
+ suite.NoError(err)
+}
+
+func (suite *DomainBlockValidateTestSuite) TestValidateDomainSubscriptionID() {
+ d := happyDomainBlock()
+
+ d.SubscriptionID = "invalid-ulid"
+ err := gtsmodel.ValidateStruct(*d)
+ suite.EqualError(err, "Key: 'DomainBlock.SubscriptionID' Error:Field validation for 'SubscriptionID' failed on the 'ulid' tag")
+
+ d.SubscriptionID = "01FEEDHX4G7EGHF5GD9E82Y51Q"
+ err = gtsmodel.ValidateStruct(*d)
+ suite.NoError(err)
+}
+
+func TestDomainBlockValidateTestSuite(t *testing.T) {
+ suite.Run(t, new(DomainBlockValidateTestSuite))
+}
diff --git a/internal/gtsmodel/emaildomainblock_test.go b/internal/gtsmodel/emaildomainblock_test.go
new file mode 100644
index 000000000..83d7c4c6a
--- /dev/null
+++ b/internal/gtsmodel/emaildomainblock_test.go
@@ -0,0 +1,96 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package gtsmodel_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func happyEmailDomainBlock() *gtsmodel.EmailDomainBlock {
+ return >smodel.EmailDomainBlock{
+ ID: "01FE91RJR88PSEEE30EV35QR8N",
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ Domain: "baddudes.suck",
+ CreatedByAccountID: "01FEED79PRMVWPRMFHFQM8MJQN",
+ }
+}
+
+type EmailDomainBlockValidateTestSuite struct {
+ suite.Suite
+}
+
+func (suite *EmailDomainBlockValidateTestSuite) TestValidateEmailDomainBlockHappyPath() {
+ // no problem here
+ e := happyEmailDomainBlock()
+ err := gtsmodel.ValidateStruct(*e)
+ suite.NoError(err)
+}
+
+func (suite *EmailDomainBlockValidateTestSuite) TestValidateEmailDomainBlockBadID() {
+ e := happyEmailDomainBlock()
+
+ e.ID = ""
+ err := gtsmodel.ValidateStruct(*e)
+ suite.EqualError(err, "Key: 'EmailDomainBlock.ID' Error:Field validation for 'ID' failed on the 'required' tag")
+
+ e.ID = "01FE96W293ZPRG9FQQP48HK8N001FE96W32AT24VYBGM12WN3GKB"
+ err = gtsmodel.ValidateStruct(*e)
+ suite.EqualError(err, "Key: 'EmailDomainBlock.ID' Error:Field validation for 'ID' failed on the 'ulid' tag")
+}
+
+func (suite *EmailDomainBlockValidateTestSuite) TestValidateEmailDomainBlockNoCreatedAt() {
+ e := happyEmailDomainBlock()
+
+ e.CreatedAt = time.Time{}
+ err := gtsmodel.ValidateStruct(*e)
+ suite.NoError(err)
+}
+
+func (suite *EmailDomainBlockValidateTestSuite) TestValidateEmailDomainBlockBadDomain() {
+ e := happyEmailDomainBlock()
+
+ e.Domain = ""
+ err := gtsmodel.ValidateStruct(*e)
+ suite.EqualError(err, "Key: 'EmailDomainBlock.Domain' Error:Field validation for 'Domain' failed on the 'required' tag")
+
+ e.Domain = "this-is-not-a-valid-domain"
+ err = gtsmodel.ValidateStruct(*e)
+ suite.EqualError(err, "Key: 'EmailDomainBlock.Domain' Error:Field validation for 'Domain' failed on the 'fqdn' tag")
+}
+
+func (suite *EmailDomainBlockValidateTestSuite) TestValidateEmailDomainBlockCreatedByAccountID() {
+ e := happyEmailDomainBlock()
+
+ e.CreatedByAccountID = ""
+ err := gtsmodel.ValidateStruct(*e)
+ suite.EqualError(err, "Key: 'EmailDomainBlock.CreatedByAccountID' Error:Field validation for 'CreatedByAccountID' failed on the 'required' tag")
+
+ e.CreatedByAccountID = "this-is-not-a-valid-ulid"
+ err = gtsmodel.ValidateStruct(*e)
+ suite.EqualError(err, "Key: 'EmailDomainBlock.CreatedByAccountID' Error:Field validation for 'CreatedByAccountID' failed on the 'ulid' tag")
+}
+
+func TestEmailDomainBlockValidateTestSuite(t *testing.T) {
+ suite.Run(t, new(EmailDomainBlockValidateTestSuite))
+}
diff --git a/internal/gtsmodel/follow.go b/internal/gtsmodel/follow.go
index 8b03db56a..c2b2633b9 100644
--- a/internal/gtsmodel/follow.go
+++ b/internal/gtsmodel/follow.go
@@ -26,10 +26,10 @@ type Follow struct {
CreatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item created
UpdatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item last updated
URI string `validate:"required,url" bun:",notnull,nullzero,unique"` // ActivityPub uri of this follow.
- AccountID string `validate:"required,ulid" bun:"type:CHAR(26),unique:srctarget,notnull"` // Who does this follow originate from?
+ AccountID string `validate:"required,ulid" bun:"type:CHAR(26),unique:srctarget,notnull"` // Who does this follow originate from?
Account *Account `validate:"-" bun:"rel:belongs-to"` // Account corresponding to accountID
- TargetAccountID string `validate:"required,ulid" bun:"type:CHAR(26),unique:srctarget,notnull"` // Who is the target of this follow ?
+ TargetAccountID string `validate:"required,ulid" bun:"type:CHAR(26),unique:srctarget,notnull"` // Who is the target of this follow ?
TargetAccount *Account `validate:"-" bun:"rel:belongs-to"` // Account corresponding to targetAccountID
- ShowReblogs bool `validate:"-" bun:",nullzero,default:true"` // Does this follow also want to see reblogs and not just posts?
+ ShowReblogs bool `validate:"-" bun:",nullzero,default:true"` // Does this follow also want to see reblogs and not just posts?
Notify bool `validate:"-" bun:",nullzero,default:false"` // does the following account want to be notified when the followed account posts?
}
diff --git a/internal/gtsmodel/followrequest.go b/internal/gtsmodel/followrequest.go
index c4b3c9997..ae22f6487 100644
--- a/internal/gtsmodel/followrequest.go
+++ b/internal/gtsmodel/followrequest.go
@@ -30,6 +30,6 @@ type FollowRequest struct {
Account *Account `validate:"-" bun:"rel:belongs-to"` // Account corresponding to accountID
TargetAccountID string `validate:"required,ulid" bun:"type:CHAR(26),unique:frsrctarget,notnull"` // Who is the target of this follow request?
TargetAccount *Account `validate:"-" bun:"rel:belongs-to"` // Account corresponding to targetAccountID
- ShowReblogs bool `validate:"-" bun:",nullzero,default:true"` // Does this follow also want to see reblogs and not just posts?
+ ShowReblogs bool `validate:"-" bun:",nullzero,default:true"` // Does this follow also want to see reblogs and not just posts?
Notify bool `validate:"-" bun:",nullzero,default:false"` // does the following account want to be notified when the followed account posts?
}
diff --git a/testrig/testmodels.go b/testrig/testmodels.go
index 002fa38c6..a7148ae17 100644
--- a/testrig/testmodels.go
+++ b/testrig/testmodels.go
@@ -261,10 +261,6 @@ func NewTestUsers() map[string]*gtsmodel.User {
// NewTestAccounts returns a map of accounts keyed by what type of account they are.
func NewTestAccounts() map[string]*gtsmodel.Account {
accounts := map[string]*gtsmodel.Account{
- "instance_account": {
- ID: "01F8MH261H1KSV3GW3016GZRY3",
- Username: "localhost:8080",
- },
"unconfirmed_account": {
ID: "01F8MH0BBE4FHXPH513MBVFHB0",
Username: "weed_lord420",
diff --git a/vendor/github.com/uptrace/bun/migrate/migration.go b/vendor/github.com/uptrace/bun/migrate/migration.go
new file mode 100644
index 000000000..79f13f972
--- /dev/null
+++ b/vendor/github.com/uptrace/bun/migrate/migration.go
@@ -0,0 +1,272 @@
+package migrate
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "fmt"
+ "io/fs"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/uptrace/bun"
+)
+
+type Migration struct {
+ bun.BaseModel
+
+ ID int64
+ Name string
+ GroupID int64
+ MigratedAt time.Time `bun:",notnull,nullzero,default:current_timestamp"`
+
+ Up MigrationFunc `bun:"-"`
+ Down MigrationFunc `bun:"-"`
+}
+
+func (m *Migration) String() string {
+ return m.Name
+}
+
+func (m *Migration) IsApplied() bool {
+ return m.ID > 0
+}
+
+type MigrationFunc func(ctx context.Context, db *bun.DB) error
+
+func NewSQLMigrationFunc(fsys fs.FS, name string) MigrationFunc {
+ return func(ctx context.Context, db *bun.DB) error {
+ isTx := strings.HasSuffix(name, ".tx.up.sql") || strings.HasSuffix(name, ".tx.down.sql")
+
+ f, err := fsys.Open(name)
+ if err != nil {
+ return err
+ }
+
+ scanner := bufio.NewScanner(f)
+ var queries []string
+
+ var query []byte
+ for scanner.Scan() {
+ b := scanner.Bytes()
+
+ const prefix = "--bun:"
+ if bytes.HasPrefix(b, []byte(prefix)) {
+ b = b[len(prefix):]
+ if bytes.Equal(b, []byte("split")) {
+ queries = append(queries, string(query))
+ query = query[:0]
+ continue
+ }
+ return fmt.Errorf("bun: unknown directive: %q", b)
+ }
+
+ query = append(query, b...)
+ query = append(query, '\n')
+ }
+
+ if len(query) > 0 {
+ queries = append(queries, string(query))
+ }
+ if err := scanner.Err(); err != nil {
+ return err
+ }
+
+ var idb bun.IConn
+
+ if isTx {
+ tx, err := db.BeginTx(ctx, nil)
+ if err != nil {
+ return err
+ }
+ idb = tx
+ } else {
+ conn, err := db.Conn(ctx)
+ if err != nil {
+ return err
+ }
+ idb = conn
+ }
+
+ for _, q := range queries {
+ _, err = idb.ExecContext(ctx, q)
+ if err != nil {
+ return err
+ }
+ }
+
+ if tx, ok := idb.(bun.Tx); ok {
+ return tx.Commit()
+ } else if conn, ok := idb.(bun.Conn); ok {
+ return conn.Close()
+ }
+
+ panic("not reached")
+ }
+}
+
+const goTemplate = `package %s
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/uptrace/bun"
+)
+
+func init() {
+ Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
+ fmt.Print(" [up migration] ")
+ return nil
+ }, func(ctx context.Context, db *bun.DB) error {
+ fmt.Print(" [down migration] ")
+ return nil
+ })
+}
+`
+
+const sqlTemplate = `SELECT 1
+
+--bun:split
+
+SELECT 2
+`
+
+//------------------------------------------------------------------------------
+
+type MigrationSlice []Migration
+
+func (ms MigrationSlice) String() string {
+ if len(ms) == 0 {
+ return "empty"
+ }
+
+ if len(ms) > 5 {
+ return fmt.Sprintf("%d migrations (%s ... %s)", len(ms), ms[0].Name, ms[len(ms)-1].Name)
+ }
+
+ var sb strings.Builder
+
+ for i := range ms {
+ if i > 0 {
+ sb.WriteString(", ")
+ }
+ sb.WriteString(ms[i].Name)
+ }
+
+ return sb.String()
+}
+
+// Applied returns applied migrations in descending order
+// (the order is important and is used in Rollback).
+func (ms MigrationSlice) Applied() MigrationSlice {
+ var applied MigrationSlice
+ for i := range ms {
+ if ms[i].IsApplied() {
+ applied = append(applied, ms[i])
+ }
+ }
+ sortDesc(applied)
+ return applied
+}
+
+// Unapplied returns unapplied migrations in ascending order
+// (the order is important and is used in Migrate).
+func (ms MigrationSlice) Unapplied() MigrationSlice {
+ var unapplied MigrationSlice
+ for i := range ms {
+ if !ms[i].IsApplied() {
+ unapplied = append(unapplied, ms[i])
+ }
+ }
+ sortAsc(unapplied)
+ return unapplied
+}
+
+// LastGroupID returns the last applied migration group id.
+// The id is 0 when there are no migration groups.
+func (ms MigrationSlice) LastGroupID() int64 {
+ var lastGroupID int64
+ for i := range ms {
+ groupID := ms[i].GroupID
+ if groupID != 0 && groupID > lastGroupID {
+ lastGroupID = groupID
+ }
+ }
+ return lastGroupID
+}
+
+// LastGroup returns the last applied migration group.
+func (ms MigrationSlice) LastGroup() *MigrationGroup {
+ group := &MigrationGroup{
+ ID: ms.LastGroupID(),
+ }
+ if group.ID == 0 {
+ return group
+ }
+ for i := range ms {
+ if ms[i].GroupID == group.ID {
+ group.Migrations = append(group.Migrations, ms[i])
+ }
+ }
+ return group
+}
+
+type MigrationGroup struct {
+ ID int64
+ Migrations MigrationSlice
+}
+
+func (g *MigrationGroup) IsZero() bool {
+ return g.ID == 0 && len(g.Migrations) == 0
+}
+
+func (g *MigrationGroup) String() string {
+ if g.IsZero() {
+ return "nil"
+ }
+ return fmt.Sprintf("group #%d (%s)", g.ID, g.Migrations)
+}
+
+type MigrationFile struct {
+ Name string
+ Path string
+ Content string
+}
+
+//------------------------------------------------------------------------------
+
+type migrationConfig struct {
+ nop bool
+}
+
+func newMigrationConfig(opts []MigrationOption) *migrationConfig {
+ cfg := new(migrationConfig)
+ for _, opt := range opts {
+ opt(cfg)
+ }
+ return cfg
+}
+
+type MigrationOption func(cfg *migrationConfig)
+
+func WithNopMigration() MigrationOption {
+ return func(cfg *migrationConfig) {
+ cfg.nop = true
+ }
+}
+
+//------------------------------------------------------------------------------
+
+func sortAsc(ms MigrationSlice) {
+ sort.Slice(ms, func(i, j int) bool {
+ return ms[i].Name < ms[j].Name
+ })
+}
+
+func sortDesc(ms MigrationSlice) {
+ sort.Slice(ms, func(i, j int) bool {
+ return ms[i].Name > ms[j].Name
+ })
+}
diff --git a/vendor/github.com/uptrace/bun/migrate/migrations.go b/vendor/github.com/uptrace/bun/migrate/migrations.go
new file mode 100644
index 000000000..9af861048
--- /dev/null
+++ b/vendor/github.com/uptrace/bun/migrate/migrations.go
@@ -0,0 +1,168 @@
+package migrate
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "strings"
+)
+
+type MigrationsOption func(m *Migrations)
+
+func WithMigrationsDirectory(directory string) MigrationsOption {
+ return func(m *Migrations) {
+ m.explicitDirectory = directory
+ }
+}
+
+type Migrations struct {
+ ms MigrationSlice
+
+ explicitDirectory string
+ implicitDirectory string
+}
+
+func NewMigrations(opts ...MigrationsOption) *Migrations {
+ m := new(Migrations)
+ for _, opt := range opts {
+ opt(m)
+ }
+ m.implicitDirectory = filepath.Dir(migrationFile())
+ return m
+}
+
+func (m *Migrations) Sorted() MigrationSlice {
+ migrations := make(MigrationSlice, len(m.ms))
+ copy(migrations, m.ms)
+ sortAsc(migrations)
+ return migrations
+}
+
+func (m *Migrations) MustRegister(up, down MigrationFunc) {
+ if err := m.Register(up, down); err != nil {
+ panic(err)
+ }
+}
+
+func (m *Migrations) Register(up, down MigrationFunc) error {
+ fpath := migrationFile()
+ name, err := extractMigrationName(fpath)
+ if err != nil {
+ return err
+ }
+
+ m.Add(Migration{
+ Name: name,
+ Up: up,
+ Down: down,
+ })
+
+ return nil
+}
+
+func (m *Migrations) Add(migration Migration) {
+ if migration.Name == "" {
+ panic("migration name is required")
+ }
+ m.ms = append(m.ms, migration)
+}
+
+func (m *Migrations) DiscoverCaller() error {
+ dir := filepath.Dir(migrationFile())
+ return m.Discover(os.DirFS(dir))
+}
+
+func (m *Migrations) Discover(fsys fs.FS) error {
+ return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if d.IsDir() {
+ return nil
+ }
+
+ if !strings.HasSuffix(path, ".up.sql") && !strings.HasSuffix(path, ".down.sql") {
+ return nil
+ }
+
+ name, err := extractMigrationName(path)
+ if err != nil {
+ return err
+ }
+
+ migration := m.getOrCreateMigration(name)
+ if err != nil {
+ return err
+ }
+ migrationFunc := NewSQLMigrationFunc(fsys, path)
+
+ if strings.HasSuffix(path, ".up.sql") {
+ migration.Up = migrationFunc
+ return nil
+ }
+ if strings.HasSuffix(path, ".down.sql") {
+ migration.Down = migrationFunc
+ return nil
+ }
+
+ return errors.New("migrate: not reached")
+ })
+}
+
+func (m *Migrations) getOrCreateMigration(name string) *Migration {
+ for i := range m.ms {
+ m := &m.ms[i]
+ if m.Name == name {
+ return m
+ }
+ }
+
+ m.ms = append(m.ms, Migration{Name: name})
+ return &m.ms[len(m.ms)-1]
+}
+
+func (m *Migrations) getDirectory() string {
+ if m.explicitDirectory != "" {
+ return m.explicitDirectory
+ }
+ if m.implicitDirectory != "" {
+ return m.implicitDirectory
+ }
+ return filepath.Dir(migrationFile())
+}
+
+func migrationFile() string {
+ const depth = 32
+ var pcs [depth]uintptr
+ n := runtime.Callers(1, pcs[:])
+ frames := runtime.CallersFrames(pcs[:n])
+
+ for {
+ f, ok := frames.Next()
+ if !ok {
+ break
+ }
+ if !strings.Contains(f.Function, "/bun/migrate.") {
+ return f.File
+ }
+ }
+
+ return ""
+}
+
+var fnameRE = regexp.MustCompile(`^(\d{14})_[0-9a-z_\-]+\.`)
+
+func extractMigrationName(fpath string) (string, error) {
+ fname := filepath.Base(fpath)
+
+ matches := fnameRE.FindStringSubmatch(fname)
+ if matches == nil {
+ return "", fmt.Errorf("migrate: unsupported migration name format: %q", fname)
+ }
+
+ return matches[1], nil
+}
diff --git a/vendor/github.com/uptrace/bun/migrate/migrator.go b/vendor/github.com/uptrace/bun/migrate/migrator.go
new file mode 100644
index 000000000..f9b4a51c2
--- /dev/null
+++ b/vendor/github.com/uptrace/bun/migrate/migrator.go
@@ -0,0 +1,401 @@
+package migrate
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "path/filepath"
+ "regexp"
+ "time"
+
+ "github.com/uptrace/bun"
+)
+
+type MigratorOption func(m *Migrator)
+
+func WithTableName(table string) MigratorOption {
+ return func(m *Migrator) {
+ m.table = table
+ }
+}
+
+func WithLocksTableName(table string) MigratorOption {
+ return func(m *Migrator) {
+ m.locksTable = table
+ }
+}
+
+type Migrator struct {
+ db *bun.DB
+ migrations *Migrations
+
+ ms MigrationSlice
+
+ table string
+ locksTable string
+}
+
+func NewMigrator(db *bun.DB, migrations *Migrations, opts ...MigratorOption) *Migrator {
+ m := &Migrator{
+ db: db,
+ migrations: migrations,
+
+ ms: migrations.ms,
+
+ table: "bun_migrations",
+ locksTable: "bun_migration_locks",
+ }
+ for _, opt := range opts {
+ opt(m)
+ }
+ return m
+}
+
+func (m *Migrator) DB() *bun.DB {
+ return m.db
+}
+
+// MigrationsWithStatus returns migrations with status in ascending order.
+func (m *Migrator) MigrationsWithStatus(ctx context.Context) (MigrationSlice, error) {
+ sorted := m.migrations.Sorted()
+
+ applied, err := m.selectAppliedMigrations(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ appliedMap := migrationMap(applied)
+ for i := range sorted {
+ m1 := &sorted[i]
+ if m2, ok := appliedMap[m1.Name]; ok {
+ m1.ID = m2.ID
+ m1.GroupID = m2.GroupID
+ m1.MigratedAt = m2.MigratedAt
+ }
+ }
+
+ return sorted, nil
+}
+
+func (m *Migrator) Init(ctx context.Context) error {
+ if _, err := m.db.NewCreateTable().
+ Model((*Migration)(nil)).
+ ModelTableExpr(m.table).
+ IfNotExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+ if _, err := m.db.NewCreateTable().
+ Model((*migrationLock)(nil)).
+ ModelTableExpr(m.locksTable).
+ IfNotExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (m *Migrator) Reset(ctx context.Context) error {
+ if _, err := m.db.NewDropTable().
+ Model((*Migration)(nil)).
+ ModelTableExpr(m.table).
+ IfExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+ if _, err := m.db.NewDropTable().
+ Model((*migrationLock)(nil)).
+ ModelTableExpr(m.locksTable).
+ IfExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+ return m.Init(ctx)
+}
+
+func (m *Migrator) Migrate(ctx context.Context, opts ...MigrationOption) (*MigrationGroup, error) {
+ cfg := newMigrationConfig(opts)
+
+ if err := m.validate(); err != nil {
+ return nil, err
+ }
+
+ if err := m.Lock(ctx); err != nil {
+ return nil, err
+ }
+ defer m.Unlock(ctx) //nolint:errcheck
+
+ migrations, err := m.MigrationsWithStatus(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ group := &MigrationGroup{
+ Migrations: migrations.Unapplied(),
+ }
+ if len(group.Migrations) == 0 {
+ return group, nil
+ }
+ group.ID = migrations.LastGroupID() + 1
+
+ for i := range group.Migrations {
+ migration := &group.Migrations[i]
+ migration.GroupID = group.ID
+
+ if !cfg.nop && migration.Up != nil {
+ if err := migration.Up(ctx, m.db); err != nil {
+ return nil, err
+ }
+ }
+
+ if err := m.MarkApplied(ctx, migration); err != nil {
+ return nil, err
+ }
+ }
+
+ return group, nil
+}
+
+func (m *Migrator) Rollback(ctx context.Context, opts ...MigrationOption) (*MigrationGroup, error) {
+ cfg := newMigrationConfig(opts)
+
+ if err := m.validate(); err != nil {
+ return nil, err
+ }
+
+ if err := m.Lock(ctx); err != nil {
+ return nil, err
+ }
+ defer m.Unlock(ctx) //nolint:errcheck
+
+ migrations, err := m.MigrationsWithStatus(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ lastGroup := migrations.LastGroup()
+
+ for i := len(lastGroup.Migrations) - 1; i >= 0; i-- {
+ migration := &lastGroup.Migrations[i]
+
+ if !cfg.nop && migration.Down != nil {
+ if err := migration.Down(ctx, m.db); err != nil {
+ return nil, err
+ }
+ }
+
+ if err := m.MarkUnapplied(ctx, migration); err != nil {
+ return nil, err
+ }
+ }
+
+ return lastGroup, nil
+}
+
+type MigrationStatus struct {
+ Migrations MigrationSlice
+ NewMigrations MigrationSlice
+ LastGroup *MigrationGroup
+}
+
+func (m *Migrator) Status(ctx context.Context) (*MigrationStatus, error) {
+ log.Printf(
+ "DEPRECATED: bun: replace Status(ctx) with " +
+ "MigrationsWithStatus(ctx)")
+
+ migrations, err := m.MigrationsWithStatus(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return &MigrationStatus{
+ Migrations: migrations,
+ NewMigrations: migrations.Unapplied(),
+ LastGroup: migrations.LastGroup(),
+ }, nil
+}
+
+func (m *Migrator) MarkCompleted(ctx context.Context) (*MigrationGroup, error) {
+ log.Printf(
+ "DEPRECATED: bun: replace MarkCompleted(ctx) with " +
+ "Migrate(ctx, migrate.WithNopMigration())")
+
+ return m.Migrate(ctx, WithNopMigration())
+}
+
+type goMigrationConfig struct {
+ packageName string
+}
+
+type GoMigrationOption func(cfg *goMigrationConfig)
+
+func WithPackageName(name string) GoMigrationOption {
+ return func(cfg *goMigrationConfig) {
+ cfg.packageName = name
+ }
+}
+
+// CreateGoMigration creates a Go migration file.
+func (m *Migrator) CreateGoMigration(
+ ctx context.Context, name string, opts ...GoMigrationOption,
+) (*MigrationFile, error) {
+ cfg := &goMigrationConfig{
+ packageName: "migrations",
+ }
+ for _, opt := range opts {
+ opt(cfg)
+ }
+
+ name, err := m.genMigrationName(name)
+ if err != nil {
+ return nil, err
+ }
+
+ fname := name + ".go"
+ fpath := filepath.Join(m.migrations.getDirectory(), fname)
+ content := fmt.Sprintf(goTemplate, cfg.packageName)
+
+ if err := ioutil.WriteFile(fpath, []byte(content), 0o644); err != nil {
+ return nil, err
+ }
+
+ mf := &MigrationFile{
+ Name: fname,
+ Path: fpath,
+ Content: content,
+ }
+ return mf, nil
+}
+
+// CreateSQLMigrations creates an up and down SQL migration files.
+func (m *Migrator) CreateSQLMigrations(ctx context.Context, name string) ([]*MigrationFile, error) {
+ name, err := m.genMigrationName(name)
+ if err != nil {
+ return nil, err
+ }
+
+ up, err := m.createSQL(ctx, name+".up.sql")
+ if err != nil {
+ return nil, err
+ }
+
+ down, err := m.createSQL(ctx, name+".down.sql")
+ if err != nil {
+ return nil, err
+ }
+
+ return []*MigrationFile{up, down}, nil
+}
+
+func (m *Migrator) createSQL(ctx context.Context, fname string) (*MigrationFile, error) {
+ fpath := filepath.Join(m.migrations.getDirectory(), fname)
+
+ if err := ioutil.WriteFile(fpath, []byte(sqlTemplate), 0o644); err != nil {
+ return nil, err
+ }
+
+ mf := &MigrationFile{
+ Name: fname,
+ Path: fpath,
+ Content: goTemplate,
+ }
+ return mf, nil
+}
+
+var nameRE = regexp.MustCompile(`^[0-9a-z_\-]+$`)
+
+func (m *Migrator) genMigrationName(name string) (string, error) {
+ const timeFormat = "20060102150405"
+
+ if name == "" {
+ return "", errors.New("migrate: migration name can't be empty")
+ }
+ if !nameRE.MatchString(name) {
+ return "", fmt.Errorf("migrate: invalid migration name: %q", name)
+ }
+
+ version := time.Now().UTC().Format(timeFormat)
+ return fmt.Sprintf("%s_%s", version, name), nil
+}
+
+// MarkApplied marks the migration as applied (applied).
+func (m *Migrator) MarkApplied(ctx context.Context, migration *Migration) error {
+ _, err := m.db.NewInsert().Model(migration).
+ ModelTableExpr(m.table).
+ Exec(ctx)
+ return err
+}
+
+// MarkUnapplied marks the migration as unapplied (new).
+func (m *Migrator) MarkUnapplied(ctx context.Context, migration *Migration) error {
+ _, err := m.db.NewDelete().
+ Model(migration).
+ ModelTableExpr(m.table).
+ Where("id = ?", migration.ID).
+ Exec(ctx)
+ return err
+}
+
+// selectAppliedMigrations selects applied (applied) migrations in descending order.
+func (m *Migrator) selectAppliedMigrations(ctx context.Context) (MigrationSlice, error) {
+ var ms MigrationSlice
+ if err := m.db.NewSelect().
+ ColumnExpr("*").
+ Model(&ms).
+ ModelTableExpr(m.table).
+ Scan(ctx); err != nil {
+ return nil, err
+ }
+ return ms, nil
+}
+
+func (m *Migrator) formattedTableName(db *bun.DB) string {
+ return db.Formatter().FormatQuery(m.table)
+}
+
+func (m *Migrator) validate() error {
+ if len(m.ms) == 0 {
+ return errors.New("migrate: there are no any migrations")
+ }
+ return nil
+}
+
+//------------------------------------------------------------------------------
+
+type migrationLock struct {
+ ID int64
+ TableName string `bun:",unique"`
+}
+
+func (m *Migrator) Lock(ctx context.Context) error {
+ lock := &migrationLock{
+ TableName: m.formattedTableName(m.db),
+ }
+ if _, err := m.db.NewInsert().
+ Model(lock).
+ ModelTableExpr(m.locksTable).
+ Exec(ctx); err != nil {
+ return fmt.Errorf("migrate: migrations table is already locked (%w)", err)
+ }
+ return nil
+}
+
+func (m *Migrator) Unlock(ctx context.Context) error {
+ tableName := m.formattedTableName(m.db)
+ _, err := m.db.NewDelete().
+ Model((*migrationLock)(nil)).
+ ModelTableExpr(m.locksTable).
+ Where("? = ?", bun.Ident("table_name"), tableName).
+ Exec(ctx)
+ return err
+}
+
+func migrationMap(ms MigrationSlice) map[string]*Migration {
+ mp := make(map[string]*Migration)
+ for i := range ms {
+ m := &ms[i]
+ mp[m.Name] = m
+ }
+ return mp
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index d98ddb2cf..f8bdb32e2 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -404,6 +404,7 @@ github.com/uptrace/bun/extra/bunjson
github.com/uptrace/bun/internal
github.com/uptrace/bun/internal/parser
github.com/uptrace/bun/internal/tagparser
+github.com/uptrace/bun/migrate
github.com/uptrace/bun/schema
# github.com/uptrace/bun/dialect/pgdialect v0.4.3
## explicit