more updates
This commit is contained in:
parent
2786b5f887
commit
7b01304dac
|
@ -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/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).
|
* [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)
|
* [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).
|
* [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).
|
* [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).
|
* [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).
|
* [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).
|
* [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).
|
* [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).
|
* [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).
|
* [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).
|
* [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).
|
* [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).
|
* [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
|
### Image Attribution
|
||||||
|
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -21,7 +21,7 @@ require (
|
||||||
github.com/go-errors/errors v1.4.0 // indirect
|
github.com/go-errors/errors v1.4.0 // indirect
|
||||||
github.com/go-fed/activity v1.0.1-0.20210803212804-d866ba75dd0f
|
github.com/go-fed/activity v1.0.1-0.20210803212804-d866ba75dd0f
|
||||||
github.com/go-fed/httpsig v1.1.0
|
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/geo v0.0.0-20210211234256-740aa86cb551 // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
|
|
|
@ -37,11 +37,13 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/cache"
|
"github.com/superseriousbusiness/gotosocial/internal/cache"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"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/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
"github.com/uptrace/bun/dialect/pgdialect"
|
"github.com/uptrace/bun/dialect/pgdialect"
|
||||||
"github.com/uptrace/bun/dialect/sqlitedialect"
|
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||||
|
"github.com/uptrace/bun/migrate"
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -73,6 +75,29 @@ type bunDBService struct {
|
||||||
conn *DBConn
|
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.
|
// 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.
|
// 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) {
|
func NewBunDBService(ctx context.Context, c *config.Config, log *logrus.Logger) (db.DB, error) {
|
||||||
|
@ -131,6 +156,9 @@ func NewBunDBService(ctx context.Context, c *config.Config, log *logrus.Logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
accounts := &accountDB{config: c, conn: conn, cache: cache.NewAccountCache()}
|
accounts := &accountDB{config: c, conn: conn, cache: cache.NewAccountCache()}
|
||||||
|
if err := doMigration(ctx, conn.DB, log); err != nil {
|
||||||
|
return nil, fmt.Errorf("db migration error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
ps := &bunDBService{
|
ps := &bunDBService{
|
||||||
Account: accounts,
|
Account: accounts,
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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**!!!!!!!!
|
|
@ -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()
|
||||||
|
)
|
|
@ -27,124 +27,56 @@ import (
|
||||||
"time"
|
"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 {
|
type Account struct {
|
||||||
/*
|
ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||||
BASIC INFO
|
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
|
||||||
// id of this account in the local database
|
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.
|
||||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
|
AvatarMediaAttachmentID string `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"` // Database ID of the media attachment, if present
|
||||||
// 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``
|
AvatarMediaAttachment *MediaAttachment `validate:"-" bun:"rel:belongs-to"` // MediaAttachment corresponding to avatarMediaAttachmentID
|
||||||
Username string `bun:",notnull,unique:userdomain,nullzero"` // username and domain should be unique *with* each other
|
AvatarRemoteURL string `validate:"omitempty,url" bun:",nullzero"` // For a non-local account, where can the header be fetched?
|
||||||
// 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.
|
HeaderMediaAttachmentID string `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"` // Database ID of the media attachment, if present
|
||||||
Domain string `bun:",unique:userdomain,nullzero"` // username and domain should be unique *with* each other
|
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.
|
||||||
ACCOUNT METADATA
|
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?
|
||||||
// ID of the avatar as a media attachment
|
AlsoKnownAs string `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"` // This account is associated with x account id
|
||||||
AvatarMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
|
MovedToAccountID string `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"` // This account has moved this account id in the database
|
||||||
AvatarMediaAttachment *MediaAttachment `bun:"rel:belongs-to"`
|
Bot bool `validate:"-" bun:",nullzero,default:false"` // Does this account identify itself as a bot?
|
||||||
// For a non-local account, where can the header be fetched?
|
Reason string `validate:"-" bun:",nullzero"` // What reason was given for signing up when this account was created?
|
||||||
AvatarRemoteURL string `bun:",nullzero"`
|
Locked bool `validate:"-" bun:",nullzero,default:true"` // Does this account need an approval for new followers?
|
||||||
// ID of the header as a media attachment
|
Discoverable bool `validate:"-" bun:",nullzero,default:false"` // Should this account be shown in the instance's profile directory?
|
||||||
HeaderMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
|
Privacy Visibility `validate:"oneof=public unlocked followers_only mutuals_only direct" bun:",nullzero,notnull,default:'public'"` // Default post privacy for this account
|
||||||
HeaderMediaAttachment *MediaAttachment `bun:"rel:belongs-to"`
|
Sensitive bool `validate:"-" bun:",nullzero,default:false"` // Set posts from this account to sensitive by default?
|
||||||
// For a non-local account, where can the header be fetched?
|
Language string `validate:"-" bun:",nullzero,notnull,default:'en'"` // What language does this account post in?
|
||||||
HeaderRemoteURL string `bun:",nullzero"`
|
URI string `validate:"required,url" bun:",nullzero,notnull,unique"` // ActivityPub URI for this account.
|
||||||
// DisplayName for this account. Can be empty, then just the Username will be used for display purposes.
|
URL string `validate:"omitempty,url" bun:",unique,nullzero"` // Web URL for this account's profile
|
||||||
DisplayName string `bun:",nullzero"`
|
LastWebfingeredAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // Last time this account was refreshed/located with webfinger.
|
||||||
// a key/value map of fields that this account has added to their profile
|
InboxURI string `validate:"omitempty,url" bun:",nullzero,unique"` // Address of this account's ActivityPub inbox, for sending activity to
|
||||||
Fields []Field
|
OutboxURI string `validate:"omitempty,url" bun:",nullzero,unique"` // Address of this account's activitypub outbox
|
||||||
// A note that this account has on their profile (ie., the account's bio/description of themselves)
|
FollowingURI string `validate:"omitempty,url" bun:",nullzero,unique"` // URI for getting the following list of this account
|
||||||
Note string `bun:",nullzero"`
|
FollowersURI string `validate:"omitempty,url" bun:",nullzero,unique"` // URI for getting the followers list of this account
|
||||||
// Is this a memorial account, ie., has the user passed away?
|
FeaturedCollectionURI string `validate:"omitempty,url" bun:",nullzero,unique"` // URL for getting the featured collection list of this account
|
||||||
Memorial bool `bun:",nullzero"`
|
ActorType string `validate:"oneof=Application Group Organization Person Service " bun:",nullzero,notnull"` // What type of activitypub actor is this account?
|
||||||
// This account has moved this account id in the database
|
PrivateKey *rsa.PrivateKey `validate:"required_without=Domain"` // Privatekey for validating activitypub requests, will only be defined for local accounts
|
||||||
MovedToAccountID string `bun:"type:CHAR(26),nullzero"`
|
PublicKey *rsa.PublicKey `validate:"required"` // Publickey for encoding activitypub requests, will be defined for both local and remote accounts
|
||||||
// When was this account created?
|
PublicKeyURI string `validate:"required" bun:",nullzero,notnull"` // Web-reachable location of this account's public key
|
||||||
CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
|
SensitizedAt time.Time `validate:"-" bun:",nullzero"` // When was this account set to have all its media shown as sensitive?
|
||||||
// When was this account last updated?
|
SilencedAt time.Time `validate:"-" bun:",nullzero"` // When was this account silenced (eg., statuses only visible to followers, not public)?
|
||||||
UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
|
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)
|
||||||
// Does this account identify itself as a bot?
|
HideCollections bool `validate:"-" bun:",nullzero,default:false"` // Hide this account's collections
|
||||||
Bot bool
|
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
|
||||||
// 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"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Field represents a key value field on an account, for things like pronouns, website, etc.
|
// 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
|
// VerifiedAt is optional, to be used only if Value is a URL to a webpage that contains the
|
||||||
// username of the user.
|
// username of the user.
|
||||||
type Field struct {
|
type Field struct {
|
||||||
Name string
|
Name string `validate:"required"` // Name of this field.
|
||||||
Value string
|
Value string `validate:"required"` // Value of this field.
|
||||||
VerifiedAt time.Time `bun:",nullzero"`
|
VerifiedAt time.Time `validate:"-" bun:",nullzero"` // This field was verified at (optional).
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,12 @@ import "time"
|
||||||
|
|
||||||
// Block refers to the blocking of one account by another.
|
// Block refers to the blocking of one account by another.
|
||||||
type Block struct {
|
type Block struct {
|
||||||
ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
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
|
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
|
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.
|
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?
|
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
|
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 ?
|
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
|
TargetAccount *Account `validate:"-" bun:"rel:belongs-to"` // Account corresponding to targetAccountID
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -26,10 +26,10 @@ type Follow struct {
|
||||||
CreatedAt time.Time `validate:"-" bun:",nullzero,notnull,default:current_timestamp"` // when was item created
|
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
|
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.
|
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
|
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
|
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?
|
Notify bool `validate:"-" bun:",nullzero,default:false"` // does the following account want to be notified when the followed account posts?
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,6 @@ type FollowRequest struct {
|
||||||
Account *Account `validate:"-" bun:"rel:belongs-to"` // Account corresponding to accountID
|
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?
|
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
|
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?
|
Notify bool `validate:"-" bun:",nullzero,default:false"` // does the following account want to be notified when the followed account posts?
|
||||||
}
|
}
|
||||||
|
|
|
@ -261,10 +261,6 @@ func NewTestUsers() map[string]*gtsmodel.User {
|
||||||
// NewTestAccounts returns a map of accounts keyed by what type of account they are.
|
// NewTestAccounts returns a map of accounts keyed by what type of account they are.
|
||||||
func NewTestAccounts() map[string]*gtsmodel.Account {
|
func NewTestAccounts() map[string]*gtsmodel.Account {
|
||||||
accounts := map[string]*gtsmodel.Account{
|
accounts := map[string]*gtsmodel.Account{
|
||||||
"instance_account": {
|
|
||||||
ID: "01F8MH261H1KSV3GW3016GZRY3",
|
|
||||||
Username: "localhost:8080",
|
|
||||||
},
|
|
||||||
"unconfirmed_account": {
|
"unconfirmed_account": {
|
||||||
ID: "01F8MH0BBE4FHXPH513MBVFHB0",
|
ID: "01F8MH0BBE4FHXPH513MBVFHB0",
|
||||||
Username: "weed_lord420",
|
Username: "weed_lord420",
|
||||||
|
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -404,6 +404,7 @@ github.com/uptrace/bun/extra/bunjson
|
||||||
github.com/uptrace/bun/internal
|
github.com/uptrace/bun/internal
|
||||||
github.com/uptrace/bun/internal/parser
|
github.com/uptrace/bun/internal/parser
|
||||||
github.com/uptrace/bun/internal/tagparser
|
github.com/uptrace/bun/internal/tagparser
|
||||||
|
github.com/uptrace/bun/migrate
|
||||||
github.com/uptrace/bun/schema
|
github.com/uptrace/bun/schema
|
||||||
# github.com/uptrace/bun/dialect/pgdialect v0.4.3
|
# github.com/uptrace/bun/dialect/pgdialect v0.4.3
|
||||||
## explicit
|
## explicit
|
||||||
|
|
Loading…
Reference in New Issue