diff options
| -rw-r--r-- | README.md | 8 | ||||
| -rw-r--r-- | go.mod | 2 | ||||
| -rw-r--r-- | internal/db/bundb/bundb.go | 29 | ||||
| -rw-r--r-- | internal/db/bundb/migrations/20210816411877_struct_validation.go | 54 | ||||
| -rw-r--r-- | internal/db/bundb/migrations/README.md | 21 | ||||
| -rw-r--r-- | internal/db/bundb/migrations/main.go | 28 | ||||
| -rw-r--r-- | internal/gtsmodel/account.go | 158 | ||||
| -rw-r--r-- | internal/gtsmodel/block.go | 16 | ||||
| -rw-r--r-- | internal/gtsmodel/block_test.go | 115 | ||||
| -rw-r--r-- | internal/gtsmodel/domainblock_test.go | 121 | ||||
| -rw-r--r-- | internal/gtsmodel/emaildomainblock_test.go | 96 | ||||
| -rw-r--r-- | internal/gtsmodel/follow.go | 6 | ||||
| -rw-r--r-- | internal/gtsmodel/followrequest.go | 2 | ||||
| -rw-r--r-- | testrig/testmodels.go | 4 | ||||
| -rw-r--r-- | vendor/github.com/uptrace/bun/migrate/migration.go | 272 | ||||
| -rw-r--r-- | vendor/github.com/uptrace/bun/migrate/migrations.go | 168 | ||||
| -rw-r--r-- | vendor/github.com/uptrace/bun/migrate/migrator.go | 401 | ||||
| -rw-r--r-- | vendor/modules.txt | 1 | 
18 files changed, 1369 insertions, 133 deletions
@@ -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 @@ -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 <http://www.gnu.org/licenses/>. +*/ + +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 <http://www.gnu.org/licenses/>. +*/ + +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 <http://www.gnu.org/licenses/>. +*/ + +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 <http://www.gnu.org/licenses/>. +*/ + +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 <http://www.gnu.org/licenses/>. +*/ + +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  | 
