diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml deleted file mode 100644 index 7a22a0a..0000000 --- a/.github/workflows/dependabot.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Dependabot auto-approve -on: pull_request - -permissions: - pull-requests: write - -jobs: - dependabot: - runs-on: ubuntu-latest - if: ${{ github.actor == 'dependabot[bot]' }} - steps: - - name: Dependabot metadata - id: metadata - uses: dependabot/fetch-metadata@v1 - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - - name: Approve a PR - run: gh pr review --approve "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml deleted file mode 100644 index 1ffed3e..0000000 --- a/.github/workflows/testing.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Go Test - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: "1.20" - - - name: Download dependencies - run: go mod download -x - - - name: Test - run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... - - - uses: codecov/codecov-action@v2 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage.txt - flags: unittests - verbose: true diff --git a/.gitignore b/.gitignore index a54ea57..8309fbd 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out -.idea \ No newline at end of file +.idea +.vscode \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c2f68e8..0000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -language: go - -go: -- 1.10.x - -env: - global: - - MONGO_URL=mongodb://travis:test@127.0.0.1:27017/mydb_test - -services: -- mongodb - -before_script: -- sleep 15 -- mongo mydb_test --eval 'db.createUser({user:"travis",pwd:"test",roles:["readWrite"]});' - -script: -- go test -v -race -coverprofile=coverage.txt -covermode=atomic -tags=integration ./... - -after_success: -- bash <(curl -s https://codecov.io/bash) diff --git a/1_global_migrate_test.go b/1_global_migrate_test.go deleted file mode 100644 index 2df991a..0000000 --- a/1_global_migrate_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package migrate - -import ( - "testing" - - "go.mongodb.org/mongo-driver/mongo" -) - -func TestGlobalMigrateSetGet(t *testing.T) { - oldMigrate := globalMigrate - defer func() { - globalMigrate = oldMigrate - }() - db := &mongo.Database{} - globalMigrate = NewMigrate(db) - - if globalMigrate.db != db { - t.Errorf("Unexpected non-equal dbs") - } - db2 := &mongo.Database{} - SetDatabase(db2) - if globalMigrate.db != db2 { - t.Errorf("Unexpected non-equal dbs") - } - SetMigrationsCollection("test") - if globalMigrate.migrationsCollection != "test" { - t.Errorf("Unexpected non-equal collections") - } -} - -func TestMigrationsRegistration(t *testing.T) { - oldMigrate := globalMigrate - defer func() { - globalMigrate = oldMigrate - }() - globalMigrate = NewMigrate(nil) - - err := Register(func(db *mongo.Database) error { - return nil - }, func(db *mongo.Database) error { - return nil - }) - if err != nil { - t.Errorf("Unexpected register error: %v", err) - return - } - registered := RegisteredMigrations() - if len(registered) <= 0 || len(registered) > 1 { - t.Errorf("Unexpected length of registered migrations") - return - } - if registered[0].Version != 1 || registered[0].Description != "global_migrate_test" { - t.Errorf("Unexpected version/description: %d %s", registered[0].Version, registered[0].Description) - } - - err = Register(func(db *mongo.Database) error { - return nil - }, func(db *mongo.Database) error { - return nil - }) - if err == nil { - t.Errorf("Unexpected nil error") - } -} - -func TestMigrationMustRegistration(t *testing.T) { - oldMigrate := globalMigrate - defer func() { - globalMigrate = oldMigrate - if r := recover(); r != nil { - t.Errorf("Unexpected panic: %v", r) - } - }() - globalMigrate = NewMigrate(nil) - MustRegister(func(db *mongo.Database) error { - return nil - }, func(db *mongo.Database) error { - return nil - }) - registered := RegisteredMigrations() - if len(registered) <= 0 || len(registered) > 1 { - t.Errorf("Unexpected length of registered migrations") - return - } - if registered[0].Version != 1 || registered[0].Description != "global_migrate_test" { - t.Errorf("Unexpected version/description: %d %s", registered[0].Version, registered[0].Description) - } -} diff --git a/1_sample_data_test.go b/1_sample_data_test.go deleted file mode 100644 index 7067e5a..0000000 --- a/1_sample_data_test.go +++ /dev/null @@ -1,28 +0,0 @@ -// +build integration - -package migrate - -import ( - "context" - - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" -) - -const globalTestCollection = "test-global" - -func init() { - Register(func(db *mongo.Database) error { - _, err := db.Collection(globalTestCollection).InsertOne(context.TODO(), bson.D{{"a", "b"}}) - if err != nil { - return err - } - return nil - }, func(db *mongo.Database) error { - _, err := db.Collection(globalTestCollection).DeleteOne(context.TODO(), bson.D{{"a", "b"}}) - if err != nil { - return err - } - return nil - }) -} diff --git a/2_sample_index_test.go b/2_sample_index_test.go deleted file mode 100644 index dd2eaf7..0000000 --- a/2_sample_index_test.go +++ /dev/null @@ -1,32 +0,0 @@ -// +build integration - -package migrate - -import ( - "context" - - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" -) - -const globalTestIndexName = "test_idx_2" - -func init() { - Register(func(db *mongo.Database) error { - keys := bson.D{{"a", 1}} - opt := options.Index().SetName(globalTestIndexName) - model := mongo.IndexModel{Keys: keys, Options: opt} - _, err := db.Collection(globalTestCollection).Indexes().CreateOne(context.TODO(), model) - if err != nil { - return err - } - return nil - }, func(db *mongo.Database) error { - _, err := db.Collection(globalTestCollection).Indexes().DropOne(context.TODO(), globalTestIndexName) - if err != nil { - return err - } - return nil - }) -} diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 1aec5df..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 Vladimir Stolyarov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md index 21e381d..0820a4e 100644 --- a/README.md +++ b/README.md @@ -1,166 +1,25 @@ -# Versioned migrations for MongoDB -[![Build Status](https://travis-ci.org/xakep666/mongo-migrate.svg?branch=master)](https://travis-ci.org/xakep666/mongo-migrate) -[![codecov](https://codecov.io/gh/xakep666/mongo-migrate/branch/master/graph/badge.svg)](https://codecov.io/gh/xakep666/mongo-migrate) -[![Go Report Card](https://goreportcard.com/badge/github.com/xakep666/mongo-migrate)](https://goreportcard.com/report/github.com/xakep666/mongo-migrate) -[![GoDoc](https://godoc.org/github.com/xakep666/mongo-migrate?status.svg)](https://godoc.org/github.com/xakep666/mongo-migrate) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +# Golang Migrations -This package allows to perform versioned migrations on your MongoDB using [mongo-go-driver](https://github.com/mongodb/mongo-go-driver). -Inspired by [go-pg migrations](https://github.com/go-pg/migrations). +Golang Migrations is an open-source Go (Golang) library for managing seeds and migrations of databases, following the principles of dependency inversion and SOLID principles. -Table of Contents -================= +## Features -* [Prerequisites](#prerequisites) -* [Installation](#installation) -* [Usage](#usage) - * [Use case \#1\. Migrations in files\.](#use-case-1-migrations-in-files) - * [Use case \#2\. Migrations in application code\.](#use-case-2-migrations-in-application-code) -* [How it works?](#how-it-works) -* [License](#license) +- Support for ![MongoDB](https://img.shields.io/badge/MongoDB-47A248?style=for-the-badge&logo=mongodb&logoColor=white) and ![MySQL](https://img.shields.io/badge/MySQL-4479A1?style=for-the-badge&logo=mysql&logoColor=white) -## Prerequisites -* Golang >= 1.10 or Vgo +- Implementation based on dependency inversion and SOLID principles. +- Easily extensible to support other databases, just implement an adapter based on an interface. ## Installation -```bash -go get -v -u github.com/xakep666/mongo-migrate -``` - -## Usage -### Use case #1. Migrations in files. - -* Create a package with migration files. -File name should be like `_.go`. - -`1_add-my-index.go` - -```go -package migrations - -import ( - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" - migrate "github.com/xakep666/mongo-migrate" -) - -func init() { - migrate.Register(func(db *mongo.Database) error { - opt := options.Index().SetName("my-index") - keys := bson.D{{"my-key", 1}} - model := mongo.IndexModel{Keys: keys, Options: opt} - _, err := db.Collection("my-coll").Indexes().CreateOne(context.TODO(), model) - if err != nil { - return err - } - - return nil - }, func(db *mongo.Database) error { - _, err := db.Collection("my-coll").Indexes().DropOne(context.TODO(), "my-index") - if err != nil { - return err - } - return nil - }) -} -``` - -* Import it in your application. -```go -import ( - ... - migrate "github.com/xakep666/mongo-migrate" - _ "path/to/migrations_package" // database migrations - ... -) -``` - -* Run migrations. -```go -func MongoConnect(host, user, password, database string) (*mongo.Database, error) { - uri := fmt.Sprintf("mongodb://%s:%s@%s:27017", user, password, host) - opt := options.Client().ApplyURI(uri) - client, err := mongo.NewClient(opt) - if err != nil { - return nil, err - } - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) - defer cancel() - err = client.Connect(ctx) - if err != nil { - return nil, err - } - db = client.Database(database) - migrate.SetDatabase(db) - if err := migrate.Up(migrate.AllAvailable); err != nil { - return nil, err - } - return db, nil -} -``` -### Use case #2. Migrations in application code. -* Just define it anywhere you want and run it. -```go -func MongoConnect(host, user, password, database string) (*mongo.Database, error) { - uri := fmt.Sprintf("mongodb://%s:%s@%s:27017", user, password, host) - opt := options.Client().ApplyURI(uri) - client, err := mongo.NewClient(opt) - if err != nil { - return nil, err - } - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) - defer cancel() - err = client.Connect(ctx) - if err != nil { - return nil, err - } - db = client.Database(database) - m := migrate.NewMigrate(db, migrate.Migration{ - Version: 1, - Description: "add my-index", - Up: func(db *mongo.Database) error { - opt := options.Index().SetName("my-index") - keys := bson.D{{"my-key", 1}} - model := mongo.IndexModel{Keys: keys, Options: opt} - _, err := db.Collection("my-coll").Indexes().CreateOne(context.TODO(), model) - if err != nil { - return err - } +To install the library, use the `go get` command: - return nil - }, - Down: func(db *mongo.Database) error { - _, err := db.Collection("my-coll").Indexes().DropOne(context.TODO(), "my-index") - if err != nil { - return err - } - return nil - }, - }) - if err := m.Up(migrate.AllAvailable); err != nil { - return nil, err - } - return db, nil -} -``` - -## How it works? -This package creates a special collection (by default it`s name is "migrations") for versioning. -In this collection stored documents like -```json -{ - "_id": "", - "version": 1, - "description": "add my-index", - "timestamp": "" -} +```bash +go get https://github.com/iamviniciuss/golang-migrations ``` -Current database version determined as version from latest inserted document. -You can change collection name using `SetMigrationsCollection` methods. -Remember that if you want to use custom collection name you need to set it before running migrations. +## Contribution +Contributions are welcome! Feel free to open issues and pull requests to improve this library. ## License -mongo-migrate project is licensed under the terms of the MIT license. Please see LICENSE in this repository for more details. +This project is licensed under the MIT License. See the LICENSE file for details. + diff --git a/bad_migration_file_test.go b/bad_migration_file_test.go deleted file mode 100644 index 06b4f9d..0000000 --- a/bad_migration_file_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package migrate - -import ( - "testing" - - "go.mongodb.org/mongo-driver/mongo" -) - -func TestBadMigrationFile(t *testing.T) { - oldMigrate := globalMigrate - defer func() { - globalMigrate = oldMigrate - }() - globalMigrate = NewMigrate(nil) - - err := Register(func(db *mongo.Database) error { - return nil - }, func(db *mongo.Database) error { - return nil - }) - if err == nil { - t.Errorf("Unexpected nil error") - } -} - -func TestBadMigrationFilePanic(t *testing.T) { - oldMigrate := globalMigrate - defer func() { - globalMigrate = oldMigrate - if r := recover(); r == nil { - t.Errorf("Unexpectedly no panic recovered") - } - }() - globalMigrate = NewMigrate(nil) - MustRegister(func(db *mongo.Database) error { - return nil - }, func(db *mongo.Database) error { - return nil - }) -} diff --git a/global_migrate.go b/global_migrate.go deleted file mode 100644 index 9ebec4f..0000000 --- a/global_migrate.go +++ /dev/null @@ -1,107 +0,0 @@ -package migrate - -import ( - "fmt" - "runtime" - - "go.mongodb.org/mongo-driver/mongo" -) - -var globalMigrate = NewMigrate(nil) - -func internalRegister(up, down MigrationFunc, skip int) error { - _, file, _, _ := runtime.Caller(skip) - version, description, err := extractVersionDescription(file) - if err != nil { - return err - } - if hasVersion(globalMigrate.migrations, version) { - return fmt.Errorf("migration with version %v already registered", version) - } - globalMigrate.migrations = append(globalMigrate.migrations, Migration{ - Version: version, - Description: description, - Up: up, - Down: down, - }) - return nil -} - -// Register performs migration registration. -// Use case of this function: -// -// - Create a file called like "1_setup_indexes.go" ("_.go"). -// -// - Use the following template inside: -// -// package migrations -// -// import ( -// "go.mongodb.org/mongo-driver/bson" -// "go.mongodb.org/mongo-driver/mongo" -// "go.mongodb.org/mongo-driver/mongo/options" -// "github.com/xakep666/mongo-migrate" -// ) -// -// func init() { -// Register(func(db *mongo.Database) error { -// opt := options.Index().SetName("my-index") -// keys := bson.D{{"my-key", 1}} -// model := mongo.IndexModel{Keys: keys, Options: opt} -// _, err := db.Collection("my-coll").Indexes().CreateOne(context.TODO(), model) -// if err != nil { -// return err -// } -// return nil -// }, func(db *mongo.Database) error { -// _, err := db.Collection("my-coll").Indexes().DropOne(context.TODO(), "my-index") -// if err != nil { -// return err -// } -// return nil -// }) -// } -func Register(up, down MigrationFunc) error { - return internalRegister(up, down, 2) -} - -// MustRegister acts like Register but panics on errors. -func MustRegister(up, down MigrationFunc) { - if err := internalRegister(up, down, 2); err != nil { - panic(err) - } -} - -// RegisteredMigrations returns all registered migrations. -func RegisteredMigrations() []Migration { - ret := make([]Migration, len(globalMigrate.migrations)) - copy(ret, globalMigrate.migrations) - return ret -} - -// SetDatabase sets database for global migrate. -func SetDatabase(db *mongo.Database) { - globalMigrate.db = db -} - -// SetMigrationsCollection changes default collection name for migrations history. -func SetMigrationsCollection(name string) { - globalMigrate.SetMigrationsCollection(name) -} - -// Version returns current database version. -func Version() (uint64, string, error) { - return globalMigrate.Version() -} - -// Up performs "up" migration using registered migrations. -// Detailed description available in Migrate.Up(). -func Up(n int) error { - return globalMigrate.Up(n) -} - -// Down performs "down" migration using registered migrations. -// Detailed description available in Migrate.Down(). -func Down(n int) error { - return globalMigrate.Down(n) -} diff --git a/global_migrate_integration_test.go b/global_migrate_integration_test.go deleted file mode 100644 index 903fc36..0000000 --- a/global_migrate_integration_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// +build integration - -package migrate - -import ( - "testing" -) - -func TestGlobalMigrateUp(t *testing.T) { - defer cleanup(db) - SetDatabase(db) - - if err := Up(-1); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - version, description, err := Version() - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if version != 2 || description != "sample_index_test" { - t.Errorf("Unexpected version/description: %v %v", version, description) - return - } -} - -func TestGlobalMigrateDown(t *testing.T) { - defer cleanup(db) - SetDatabase(db) - - if err := Up(-1); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if err := Down(-1); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - version, _, err := Version() - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if version != 0 { - t.Errorf("Unexpected version: %v", version) - return - } -} diff --git a/go.mod b/go.mod index c9fcea9..6bf33f2 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,35 @@ -module github.com/xakep666/mongo-migrate +module github.com/iamviniciuss/golang-migrations + +go 1.20 require ( + github.com/golang-migrate/migrate/v4 v4.16.2 github.com/pkg/errors v0.9.1 - go.mongodb.org/mongo-driver v1.5.1 + github.com/stretchr/testify v1.9.0 + github.com/tryvium-travels/memongo v0.12.0 + go.mongodb.org/mongo-driver/v2 v2.3.0 ) require ( - github.com/aws/aws-sdk-go v1.34.28 // indirect + github.com/acobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-sql-driver/mysql v1.5.0 // indirect github.com/go-stack/stack v1.8.0 // indirect - github.com/golang/snappy v0.0.1 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/klauspost/compress v1.9.5 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/afero v1.6.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect - github.com/xdg-go/scram v1.0.2 // indirect - github.com/xdg-go/stringprep v1.0.2 // indirect - github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect - golang.org/x/crypto v0.1.0 // indirect - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect - golang.org/x/text v0.4.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + go.mongodb.org/mongo-driver v1.9.1 // indirect + go.uber.org/atomic v1.7.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/text v0.22.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 809c04a..47f384b 100644 --- a/go.sum +++ b/go.sum @@ -1,128 +1,135 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/aws/aws-sdk-go v1.34.28 h1:sscPpn/Ns3i0F4HPEWAVcwdIRaZZCuL7llJ2/60yPIk= -github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/acobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249 h1:fMi9ZZ/it4orHj3xWrM6cLkVFcCbkXQALFUiNtHtCPs= +github.com/acobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249/go.mod h1:iU1PxQMQwoHZZWmMKrMkrNlY+3+p9vxIjpZOVyxWa0g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dhui/dktest v0.3.16 h1:i6gq2YQEtcrjKbeJpBkWjE8MmLZPYllcjOFbTZuPDnw= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= -github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= -github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= -github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= -github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= -github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= -github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= -github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= -github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= -github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= -github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= -github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= -github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= -github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= -github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= -github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= -github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= -github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= -github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= -github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= -github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= -github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= -github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= -github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= -github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA= +github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= -github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= -github.com/klauspost/compress v1.9.5 h1:U+CaK85mrNNb4k8BNOfgJtJ/gr6kswUCFj6miSzVC6M= -github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= -github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tryvium-travels/memongo v0.12.0 h1:B56+Do7Z3vcR93oqkyUubvdFPJEqpHn1ZBSQRYe4Nnk= +github.com/tryvium-travels/memongo v0.12.0/go.mod h1:riRUHKRQ5JbeX2ryzFfmr7P2EYXIkNwgloSQJPpBikA= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= -github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -go.mongodb.org/mongo-driver v1.5.1 h1:9nOVLGDfOaZ9R0tBumx/BcuqkbFpyTCU2r/Po7A2azI= -go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.9.1 h1:m078y9v7sBItkt1aaoe2YlvWEXcD263e1a4E1fBrJ1c= +go.mongodb.org/mongo-driver v1.9.1/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= +go.mongodb.org/mongo-driver/v2 v2.3.0 h1:sh55yOXA2vUjW1QYw/2tRlHSQViwDyPnW61AwpZ4rtU= +go.mongodb.org/mongo-driver/v2 v2.3.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/migrate.go b/migrate.go deleted file mode 100644 index b1025e9..0000000 --- a/migrate.go +++ /dev/null @@ -1,236 +0,0 @@ -// Package migrate allows to perform versioned migrations in your MongoDB. -package migrate - -import ( - "context" - "time" - - "github.com/pkg/errors" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" -) - -type collectionSpecification struct { - Name string `bson:"name"` - Type string `bson:"type"` -} - -type versionRecord struct { - Version uint64 `bson:"version"` - Description string `bson:"description,omitempty"` - Timestamp time.Time `bson:"timestamp"` -} - -const defaultMigrationsCollection = "migrations" - -// AllAvailable used in "Up" or "Down" methods to run all available migrations. -const AllAvailable = -1 - -// Migrate is type for performing migrations in provided database. -// Database versioned using dedicated collection. -// Each migration applying ("up" and "down") adds new document to collection. -// This document consists migration version, migration description and timestamp. -// Current database version determined as version in latest added document (biggest "_id") from collection mentioned above. -type Migrate struct { - db *mongo.Database - migrations []Migration - migrationsCollection string -} - -func NewMigrate(db *mongo.Database, migrations ...Migration) *Migrate { - internalMigrations := make([]Migration, len(migrations)) - copy(internalMigrations, migrations) - return &Migrate{ - db: db, - migrations: internalMigrations, - migrationsCollection: defaultMigrationsCollection, - } -} - -// SetMigrationsCollection replaces name of collection for storing migration information. -// By default it is "migrations". -func (m *Migrate) SetMigrationsCollection(name string) { - m.migrationsCollection = name -} - -func (m *Migrate) isCollectionExist(name string) (isExist bool, err error) { - collections, err := m.getCollections() - if err != nil { - return false, err - } - - for _, c := range collections { - if name == c.Name { - return true, nil - } - } - return false, nil -} - -func (m *Migrate) createCollectionIfNotExist(name string) error { - exist, err := m.isCollectionExist(name) - if err != nil { - return err - } - if exist { - return nil - } - - command := bson.D{bson.E{Key: "create", Value: name}} - err = m.db.RunCommand(nil, command).Err() - if err != nil { - return err - } - - return nil -} - -func (m *Migrate) getCollections() (collections []collectionSpecification, err error) { - filter := bson.D{bson.E{Key: "type", Value: "collection"}} - options := options.ListCollections().SetNameOnly(true) - - cursor, err := m.db.ListCollections(context.Background(), filter, options) - if err != nil { - return nil, err - } - - if cursor != nil { - defer func(cursor *mongo.Cursor) { - curErr := cursor.Close(context.TODO()) - if curErr != nil { - if err != nil { - err = errors.Wrapf(curErr, "migrate: get collection failed: %s", err.Error()) - } else { - err = curErr - } - } - }(cursor) - } - - for cursor.Next(context.TODO()) { - var collection collectionSpecification - - err := cursor.Decode(&collection) - if err != nil { - return nil, err - } - - collections = append(collections, collection) - } - - if err := cursor.Err(); err != nil { - return nil, err - } - - return -} - -// Version returns current database version and comment. -func (m *Migrate) Version() (uint64, string, error) { - if err := m.createCollectionIfNotExist(m.migrationsCollection); err != nil { - return 0, "", err - } - - filter := bson.D{{}} - sort := bson.D{bson.E{Key: "_id", Value: -1}} - options := options.FindOne().SetSort(sort) - - // find record with greatest id (assuming it`s latest also) - result := m.db.Collection(m.migrationsCollection).FindOne(context.TODO(), filter, options) - err := result.Err() - switch { - case err == mongo.ErrNoDocuments: - return 0, "", nil - case err != nil: - return 0, "", err - } - - var rec versionRecord - if err := result.Decode(&rec); err != nil { - return 0, "", err - } - - return rec.Version, rec.Description, nil -} - -// SetVersion forcibly changes database version to provided. -func (m *Migrate) SetVersion(version uint64, description string) error { - rec := versionRecord{ - Version: version, - Timestamp: time.Now().UTC(), - Description: description, - } - - _, err := m.db.Collection(m.migrationsCollection).InsertOne(context.TODO(), rec) - if err != nil { - return err - } - - return nil -} - -// Up performs "up" migrations to latest available version. -// If n<=0 all "up" migrations with newer versions will be performed. -// If n>0 only n migrations with newer version will be performed. -func (m *Migrate) Up(n int) error { - currentVersion, _, err := m.Version() - if err != nil { - return err - } - if n <= 0 || n > len(m.migrations) { - n = len(m.migrations) - } - migrationSort(m.migrations) - - for i, p := 0, 0; i < len(m.migrations) && p < n; i++ { - migration := m.migrations[i] - if migration.Version <= currentVersion || migration.Up == nil { - continue - } - p++ - if err := migration.Up(m.db); err != nil { - return err - } - if err := m.SetVersion(migration.Version, migration.Description); err != nil { - return err - } - } - return nil -} - -// Down performs "down" migration to oldest available version. -// If n<=0 all "down" migrations with older version will be performed. -// If n>0 only n migrations with older version will be performed. -func (m *Migrate) Down(n int) error { - currentVersion, _, err := m.Version() - if err != nil { - return err - } - if n <= 0 || n > len(m.migrations) { - n = len(m.migrations) - } - migrationSort(m.migrations) - - for i, p := len(m.migrations)-1, 0; i >= 0 && p < n; i-- { - migration := m.migrations[i] - if migration.Version > currentVersion || migration.Down == nil { - continue - } - p++ - if err := migration.Down(m.db); err != nil { - return err - } - - var prevMigration Migration - if i == 0 { - prevMigration = Migration{Version: 0} - } else { - prevMigration = m.migrations[i-1] - } - if err := m.SetVersion(prevMigration.Version, prevMigration.Description); err != nil { - return err - } - } - return nil -} diff --git a/migration.go b/migration.go deleted file mode 100644 index 9f617fa..0000000 --- a/migration.go +++ /dev/null @@ -1,42 +0,0 @@ -package migrate - -import ( - "sort" - - "go.mongodb.org/mongo-driver/mongo" -) - -// MigrationFunc used to define actions to be performed for a migration. -type MigrationFunc func(db *mongo.Database) error - -// Migration represents single database migration. -// Migration contains: -// -// - version: migration version, must be unique in migration list -// -// - description: text description of migration -// -// - up: callback which will be called in "up" migration process -// -// - down: callback which will be called in "down" migration process for reverting changes -type Migration struct { - Version uint64 - Description string - Up MigrationFunc - Down MigrationFunc -} - -func migrationSort(migrations []Migration) { - sort.Slice(migrations, func(i, j int) bool { - return migrations[i].Version < migrations[j].Version - }) -} - -func hasVersion(migrations []Migration, version uint64) bool { - for _, m := range migrations { - if m.Version == version { - return true - } - } - return false -} diff --git a/migration_integration_test.go b/migration_integration_test.go deleted file mode 100644 index 2c16c69..0000000 --- a/migration_integration_test.go +++ /dev/null @@ -1,624 +0,0 @@ -// +build integration - -package migrate - -import ( - "context" - "errors" - "net/url" - "os" - "strings" - "testing" - "time" - - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" -) - -const testCollection = "test" - -type index struct { - Key map[string]int - NS string - Name string -} - -func cleanup(db *mongo.Database) { - filter := bson.D{bson.E{Key: "type", Value: "collection"}} - options := options.ListCollections().SetNameOnly(true) - - cursor, err := db.ListCollections(context.Background(), filter, options) - if err != nil { - panic(err) - } - - defer cursor.Close(context.TODO()) - - var collections []collectionSpecification - - for cursor.Next(context.TODO()) { - var collection collectionSpecification - - err := cursor.Decode(&collection) - if err != nil { - panic(err) - } - - collections = append(collections, collection) - } - - if err := cursor.Err(); err != nil { - panic(err) - } - - for _, collection := range collections { - _, err := db.Collection(collection.Name).Indexes().DropAll(context.TODO()) - if err != nil { - panic(err) - } - err = db.Collection(collection.Name).Drop(context.TODO()) - if err != nil { - panic(err) - } - } -} - -var db *mongo.Database - -func TestMain(m *testing.M) { - addr, err := url.Parse(os.Getenv("MONGO_URL")) - opt := options.Client().ApplyURI(addr.String()) - client, err := mongo.NewClient(opt) - if err != nil { - panic(err) - } - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) - defer cancel() - err = client.Connect(ctx) - if err != nil { - panic(err) - } - db = client.Database(strings.TrimLeft(addr.Path, "/")) - defer cleanup(db) - os.Exit(m.Run()) -} - -func TestSetGetVersion(t *testing.T) { - defer cleanup(db) - migrate := NewMigrate(db) - if err := migrate.SetVersion(1, "hello"); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - version, description, err := migrate.Version() - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if version != 1 || description != "hello" { - t.Errorf("Unexpected version/description %v %v", version, description) - return - } - - if err := migrate.SetVersion(2, "world"); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - version, description, err = migrate.Version() - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if version != 2 || description != "world" { - t.Errorf("Unexpected version/description %v %v", version, description) - return - } - - if err := migrate.SetVersion(1, "hello"); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - version, description, err = migrate.Version() - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if version != 1 || description != "hello" { - t.Errorf("Unexpected version/description %v %v", version, description) - return - } -} - -func TestVersionBeforeSet(t *testing.T) { - defer cleanup(db) - migrate := NewMigrate(db) - version, _, err := migrate.Version() - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if version != 0 { - t.Errorf("Unexpected version: %v", err) - return - } -} - -func TestUpMigrations(t *testing.T) { - defer cleanup(db) - migrate := NewMigrate(db, - Migration{Version: 1, Description: "hello", Up: func(db *mongo.Database) error { - _, err := db.Collection(testCollection).InsertOne(context.TODO(), bson.D{{"hello", "world"}}) - if err != nil { - return err - } - - return nil - }}, - Migration{Version: 2, Description: "world", Up: func(db *mongo.Database) error { - opt := options.Index().SetName("test_idx") - keys := bson.D{{"hello", 1}} - model := mongo.IndexModel{Keys: keys, Options: opt} - _, err := db.Collection(testCollection).Indexes().CreateOne(context.TODO(), model) - if err != nil { - return err - } - - return nil - }}, - ) - if err := migrate.Up(AllAvailable); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - version, description, err := migrate.Version() - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if version != 2 || description != "world" { - t.Errorf("Unexpected version/description %v %v", version, description) - return - } - result := db.Collection(testCollection).FindOne(context.TODO(), bson.D{{"hello", "world"}}) - if result.Err() != nil { - t.Errorf("Unexpected error: %v", err) - return - } - doc := bson.D{} - if err := result.Decode(&doc); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if doc.Map()["hello"].(string) != "world" { - t.Errorf("Unexpected data") - return - } - cursor, err := db.Collection(testCollection).Indexes().List(context.TODO()) - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - defer cursor.Close(context.TODO()) - - var indexes []index - for cursor.Next(context.TODO()) { - var index index - - err := cursor.Decode(&index) - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - - indexes = append(indexes, index) - } - - if err := cursor.Err(); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - - for _, v := range indexes { - if v.Name == "test_idx" { - return - } - } - - t.Errorf("Expected index not found") -} - -func TestDownMigrations(t *testing.T) { - defer cleanup(db) - migrate := NewMigrate(db, - Migration{Version: 1, Description: "hello", Up: func(db *mongo.Database) error { - _, err := db.Collection(testCollection).InsertOne(context.TODO(), bson.D{{"hello", "world"}}) - if err != nil { - return err - } - return nil - }, Down: func(db *mongo.Database) error { - _, err := db.Collection(testCollection).DeleteOne(context.TODO(), bson.D{{"hello", "world"}}) - if err != nil { - return err - } - return nil - }}, - Migration{Version: 2, Description: "world", Up: func(db *mongo.Database) error { - opt := options.Index().SetName("test_idx") - keys := bson.D{{"hello", 1}} - model := mongo.IndexModel{Keys: keys, Options: opt} - _, err := db.Collection(testCollection).Indexes().CreateOne(context.TODO(), model) - if err != nil { - return err - } - - return nil - }, Down: func(db *mongo.Database) error { - _, err := db.Collection(testCollection).Indexes().DropOne(context.TODO(), "test_idx") - if err != nil { - return err - } - return nil - }}, - ) - if err := migrate.Up(AllAvailable); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if err := migrate.Down(AllAvailable); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - version, _, err := migrate.Version() - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if version != 0 { - t.Errorf("Unexpected version: %v", version) - return - } - result := db.Collection(testCollection).FindOne(context.TODO(), bson.D{{"hello", "world"}}) - if err := result.Decode(&bson.D{}); err != mongo.ErrNoDocuments { - t.Errorf("Unexpected error: %v", err) - return - } - cursor, err := db.Collection(testCollection).Indexes().List(context.TODO()) - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - defer cursor.Close(context.TODO()) - - var indexes []index - for cursor.Next(context.TODO()) { - var index index - - err := cursor.Decode(&index) - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - - indexes = append(indexes, index) - } - - if err := cursor.Err(); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - - for _, v := range indexes { - if v.Name == "test_idx" { - t.Errorf("Index unexpectedly found") - return - } - } -} - -func TestPartialUpMigrations(t *testing.T) { - defer cleanup(db) - migrate := NewMigrate(db, - Migration{Version: 1, Description: "hello", Up: func(db *mongo.Database) error { - _, err := db.Collection(testCollection).InsertOne(context.TODO(), bson.D{{"hello", "world"}}) - if err != nil { - return err - } - return nil - }}, - Migration{Version: 2, Description: "world", Up: func(db *mongo.Database) error { - opt := options.Index().SetName("test_idx") - keys := bson.D{{"hello", 1}} - model := mongo.IndexModel{Keys: keys, Options: opt} - _, err := db.Collection(testCollection).Indexes().CreateOne(context.TODO(), model) - if err != nil { - return err - } - return nil - }}, - Migration{Version: 3, Description: "shouldn`t be applied", Up: func(db *mongo.Database) error { - _, err := db.Collection(testCollection).InsertOne(context.TODO(), bson.D{{"a", "b"}}) - if err != nil { - return err - } - return nil - }}, - ) - if err := migrate.Up(2); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - version, description, err := migrate.Version() - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if version != 2 || description != "world" { - t.Errorf("Unexpected version/description %v %v", version, description) - return - } - result := db.Collection(testCollection).FindOne(context.TODO(), bson.D{{"hello", "world"}}) - if err := result.Err(); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - var doc bson.D - err = result.Decode(&doc) - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if doc.Map()["hello"].(string) != "world" { - t.Errorf("Unexpected data") - return - } - cursor, err := db.Collection(testCollection).Indexes().List(context.TODO()) - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - defer cursor.Close(context.TODO()) - - var indexes []index - for cursor.Next(context.TODO()) { - var index index - - err := cursor.Decode(&index) - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - - indexes = append(indexes, index) - } - - if err := cursor.Err(); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - - for _, index := range indexes { - if index.Name == "test_idx" { - goto okIndex - } - } - t.Errorf("Expected index not found") -okIndex: - res := db.Collection(testCollection).FindOne(context.TODO(), bson.D{{"a", "b"}}) - if err := res.Decode(&bson.D{}); err != mongo.ErrNoDocuments { - t.Errorf("Unexpectedly found data from non-applied migration") - return - } -} - -func TestPartialDownMigrations(t *testing.T) { - defer cleanup(db) - migrate := NewMigrate(db, - Migration{Version: 1, Description: "hello", Up: func(db *mongo.Database) error { - _, err := db.Collection(testCollection).InsertOne(context.TODO(), bson.D{{"hello", "world"}}) - if err != nil { - return err - } - return nil - }, Down: func(db *mongo.Database) error { - _, err := db.Collection(testCollection).DeleteOne(context.TODO(), bson.D{{"hello", "world"}}) - if err != nil { - return err - } - return nil - }}, - Migration{Version: 2, Description: "world", Up: func(db *mongo.Database) error { - keys := bson.D{{"hello", 1}} - opt := options.Index().SetName("test_idx") - model := mongo.IndexModel{Keys: keys, Options: opt} - _, err := db.Collection(testCollection).Indexes().CreateOne(context.TODO(), model) - if err != nil { - return err - } - return err - }, Down: func(db *mongo.Database) error { - _, err := db.Collection(testCollection).Indexes().DropOne(context.TODO(), "test_idx") - if err != nil { - return err - } - return nil - }}, - Migration{Version: 3, Description: "next", Up: func(db *mongo.Database) error { - _, err := db.Collection(testCollection).InsertOne(context.TODO(), bson.D{{"a", "b"}}) - if err != nil { - return err - } - return nil - }, Down: func(db *mongo.Database) error { - _, err := db.Collection(testCollection).DeleteOne(context.TODO(), bson.D{{"a", "b"}}) - if err != nil { - return err - } - return nil - }}, - ) - if err := migrate.Up(AllAvailable); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - result := db.Collection(testCollection).FindOne(context.TODO(), bson.D{{"a", "b"}}) - if err := result.Err(); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if err := migrate.Down(1); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - version, description, err := migrate.Version() - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if version != 2 || description != "world" { - t.Errorf("Unexpected version/description: %v %v", version, description) - return - } - res := db.Collection(testCollection).FindOne(context.TODO(), bson.D{{"a", "b"}}) - if err := res.Decode(&bson.D{}); err != mongo.ErrNoDocuments { - t.Errorf("Unexpected error: %v", err) - return - } -} - -func TestUpMigrationWithErrors(t *testing.T) { - defer cleanup(db) - expectedErr := errors.New("normal error") - migrate := NewMigrate(db, - Migration{Version: 1, Description: "hello", Up: func(db *mongo.Database) error { - return expectedErr - }}, - ) - if err := migrate.Up(AllAvailable); err != expectedErr { - t.Errorf("Unexpected error: %v", err) - return - } - version, _, err := migrate.Version() - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if version != 0 { - t.Errorf("Unexpected version: %v", version) - return - } -} - -func TestDownMigrationWithErrors(t *testing.T) { - defer cleanup(db) - expectedErr := errors.New("normal error") - migrate := NewMigrate(db, - Migration{Version: 1, Description: "hello", Up: func(db *mongo.Database) error { - _, err := db.Collection(testCollection).InsertOne(context.TODO(), bson.D{{"hello", "world"}}) - if err != nil { - return err - } - return nil - }, Down: func(db *mongo.Database) error { - return expectedErr - }}, - ) - if err := migrate.Up(AllAvailable); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if err := migrate.Down(AllAvailable); err != expectedErr { - t.Errorf("Unexpected error: %v", err) - return - } - version, _, err := migrate.Version() - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if version != 1 { - t.Errorf("Unexpected version: %v", version) - return - } -} - -func TestMultipleUpMigration(t *testing.T) { - defer cleanup(db) - var cnt int - migrate := NewMigrate(db, - Migration{Version: 1, Description: "hello", Up: func(db *mongo.Database) error { - cnt++ - return nil - }}, - Migration{Version: 2, Description: "world", Up: func(db *mongo.Database) error { - cnt++ - return nil - }}, - ) - if err := migrate.Up(AllAvailable); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if err := migrate.Up(AllAvailable); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - version, description, err := migrate.Version() - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if version != 2 || description != "world" { - t.Errorf("Unexpected version/description %v %v", version, description) - return - } - if cnt != 2 { - t.Errorf("Unexpected apply call count: %v", cnt) - return - } -} - -func TestMultipleDownMigration(t *testing.T) { - defer cleanup(db) - var cnt int - migrate := NewMigrate(db, - Migration{Version: 1, Description: "hello", Up: func(db *mongo.Database) error { - return nil - }, Down: func(db *mongo.Database) error { - cnt++ - return nil - }}, - Migration{Version: 2, Description: "world", Up: func(db *mongo.Database) error { - return nil - }, Down: func(db *mongo.Database) error { - cnt++ - return nil - }}, - ) - if err := migrate.Up(AllAvailable); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if err := migrate.Down(AllAvailable); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if err := migrate.Down(AllAvailable); err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - version, _, err := migrate.Version() - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - if version != 0 { - t.Errorf("Unexpected version: %v", version) - return - } - if cnt != 2 { - t.Errorf("Unexpected apply call count: %v", cnt) - return - } -} diff --git a/migration_test.go b/migration_test.go deleted file mode 100644 index 4137303..0000000 --- a/migration_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package migrate - -import ( - "sort" - "testing" -) - -func TestMigrationSort(t *testing.T) { - migrations := []Migration{ - {Version: 10, Description: "10"}, - {Version: 2, Description: "2"}, - {Version: 4, Description: "4"}, - {Version: 8, Description: "8"}, - } - migrationSort(migrations) - if !sort.SliceIsSorted(migrations, func(i, j int) bool { - return migrations[i].Version < migrations[j].Version - }) { - t.Errorf("Unexpected unsorted array") - } -} - -func TestHasVersion(t *testing.T) { - migrations := []Migration{ - {Version: 10, Description: "10"}, - {Version: 2, Description: "2"}, - {Version: 4, Description: "4"}, - {Version: 8, Description: "8"}, - } - if !hasVersion(migrations, 2) { - t.Errorf("Unexpectedly not found version") - } - if hasVersion(migrations, 3) { - t.Errorf("Unexpectedly found version") - } -} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c682e30 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "contact_api", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test-generate-coverfile": "go test -coverprofile=coverage.out ./src/...", + "test-exec-cover-html": "go tool cover -func=coverage.out", + "test-export-html": "go tool cover -html=coverage.out", + "test": "npm run test-generate-coverfile && npm run test-exec-cover-html && npm run test-export-html", + "go-path": "PATH=$(go env GOPATH)/bin:$PATH", + "api-docs": "swag init -g src/main.go --output src/docs" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/crewhu/contact_api.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/crewhu/contact_api/issues" + }, + "homepage": "https://github.com/crewhu/contact_api#readme" +} diff --git a/src/adapter/migration_handler.go b/src/adapter/migration_handler.go new file mode 100644 index 0000000..afeb549 --- /dev/null +++ b/src/adapter/migration_handler.go @@ -0,0 +1,9 @@ +package adapter + +type MigrationHandler interface { + GetVersion() uint64 + GetType() string + GetName() string + Up() error + Down() error +} diff --git a/src/adapter/migration_repository.go b/src/adapter/migration_repository.go new file mode 100644 index 0000000..4f8d162 --- /dev/null +++ b/src/adapter/migration_repository.go @@ -0,0 +1,11 @@ +package adapter + +import ( + entity "github.com/iamviniciuss/golang-migrations/src/dto" +) + +type MigrationRepository interface { + Insert(rec *entity.VersionRecord) (*entity.VersionRecord, error) + FindOne(rec *entity.VersionRecord) (*entity.VersionRecord, error) + CreateCollectionIfNotExists(name string) error +} diff --git a/src/dto/collection.go b/src/dto/collection.go new file mode 100644 index 0000000..bd5ada5 --- /dev/null +++ b/src/dto/collection.go @@ -0,0 +1,6 @@ +package entity + +type CollectionSpecification struct { + Name string `bson:"name"` + Type string `bson:"type"` +} diff --git a/src/dto/record.go b/src/dto/record.go new file mode 100644 index 0000000..d1213d2 --- /dev/null +++ b/src/dto/record.go @@ -0,0 +1,10 @@ +package entity + +import "time" + +type VersionRecord struct { + Type string `bson:"type"` + Version uint64 `bson:"version"` + Description string `bson:"description,omitempty"` + Timestamp time.Time `bson:"timestamp"` +} diff --git a/src/pkg/migrate.go b/src/pkg/migrate.go new file mode 100644 index 0000000..46c428c --- /dev/null +++ b/src/pkg/migrate.go @@ -0,0 +1,151 @@ +package migrate + +import ( + "fmt" + "time" + + repository "github.com/iamviniciuss/golang-migrations/src/adapter" + entity "github.com/iamviniciuss/golang-migrations/src/dto" +) + +const defaultMigrationsCollection = "migrations" + +const AllAvailable = -1 + +type Migrate struct { + migrationRepository repository.MigrationRepository + migrations []Migration + migrationsCollection string +} + +func NewMigrate(migrationRepository repository.MigrationRepository, migrations ...Migration) *Migrate { + internalMigrations := make([]Migration, len(migrations)) + copy(internalMigrations, migrations) + return &Migrate{ + migrations: internalMigrations, + migrationsCollection: defaultMigrationsCollection, + migrationRepository: migrationRepository, + } +} + +func (m *Migrate) SetMigrationsCollection(name string) { + m.migrationsCollection = name +} + +func (m *Migrate) Version(rec *entity.VersionRecord) (uint64, string, error) { + if err := m.migrationRepository.CreateCollectionIfNotExists(m.migrationsCollection); err != nil { + return 0, "", err + } + + rec, err := m.migrationRepository.FindOne(rec) + + if err != nil { + return 0, "", nil + } + + return rec.Version, rec.Description, nil +} + +func (m *Migrate) SetVersion(version uint64, description string, typing string) error { + rec := &entity.VersionRecord{ + Version: version, + Timestamp: time.Now().UTC(), + Description: description, + Type: typing, + } + + _, err := m.migrationRepository.Insert(rec) + + return err +} + +func (m *Migrate) Up(n int) error { + + if n <= 0 || n > len(m.migrations) { + n = len(m.migrations) + } + migrationSort(m.migrations) + + for i, p := 0, 0; i < len(m.migrations) && p < n; i++ { + migration := m.migrations[i] + currentVersion, _, err := m.Version(&entity.VersionRecord{ + Type: migration.Handler.GetType(), + Version: migration.Version, + Description: migration.Handler.GetName(), + }) + + if err != nil { + return err + } + + if migration.Version <= currentVersion || migration.Handler == nil { + continue + } + p++ + if err := migration.Handler.Up(); err != nil { + return err + } + if err := m.SetVersion(migration.Version, migration.Description, migration.Handler.GetType()); err != nil { + return err + } + } + return nil +} + +func (m *Migrate) Down(n int) error { + + if n <= 0 || n > len(m.migrations) { + n = len(m.migrations) + } + migrationSort(m.migrations) + + for i, p := len(m.migrations)-1, 0; i >= 0 && p < n; i-- { + migration := m.migrations[i] + + currentVersion, _, err := m.Version(&entity.VersionRecord{ + Type: migration.Handler.GetType(), + Version: migration.Version, + Description: migration.Handler.GetName(), + }) + + if err != nil { + return err + } + + if migration.Version > currentVersion || migration.Handler == nil { + continue + } + p++ + if err := migration.Handler.Down(); err != nil { + return err + } + + var prevMigration Migration + if i == 0 { + prevMigration = Migration{Version: 0} + } else { + prevMigration = m.migrations[i-1] + } + if err := m.SetVersion(prevMigration.Version, prevMigration.Description, migration.Handler.GetType()); err != nil { + return err + } + } + return nil +} + +func (mig *Migrate) internalRegister(migrationHandler repository.MigrationHandler, skip int) error { + if hasVersion(mig.migrations, migrationHandler) { + return fmt.Errorf("migration with version %v already registered", migrationHandler.GetVersion()) + } + + mig.migrations = append(mig.migrations, Migration{ + Version: migrationHandler.GetVersion(), + Description: migrationHandler.GetName(), + Handler: migrationHandler, + }) + return nil +} + +func (mig *Migrate) Register(migrationHandler repository.MigrationHandler) error { + return mig.internalRegister(migrationHandler, 2) +} diff --git a/src/pkg/migrate_test.go b/src/pkg/migrate_test.go new file mode 100644 index 0000000..f7e0242 --- /dev/null +++ b/src/pkg/migrate_test.go @@ -0,0 +1,196 @@ +package migrate + +import ( + "errors" + "testing" + "time" + + entity "github.com/iamviniciuss/golang-migrations/src/dto" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +type MigrationManagerTestSuite struct { + suite.Suite + value int + repo *MigrationRepositoryMock +} + +// SetupSuite é executado uma vez antes de todos os testes da suíte. +func (suite *MigrationManagerTestSuite) SetupSuite() { + // Configuração global + suite.value = 42 +} + +// SetupTest é executado antes de cada teste. +func (suite *MigrationManagerTestSuite) SetupTest() { + // Configuração específica para cada teste + suite.repo = new(MigrationRepositoryMock) +} + +// TearDownTest é executado depois de cada teste. +func (suite *MigrationManagerTestSuite) TearDownTest() { + // Limpeza específica para cada teste +} + +// TearDownSuite é executado uma vez após todos os testes da suíte. +func (suite *MigrationManagerTestSuite) TearDownSuite() { + // Limpeza global +} + +func (suite *MigrationManagerTestSuite) TestBasicTestUpgradeMigrationVersion() { + migrateHandler := &MigrationHandlerMock{} + migrateHandler.On("GetVersion").Return(uint64(2)) + migrateHandler.On("GetName").Return("BasicTest") + migrateHandler.On("GetType").Return("migration") + migrateHandler.On("Up").Return(nil) + + suite.repo.On("CreateCollectionIfNotExists", mock.Anything).Return(nil) + suite.repo.On("Insert", mock.Anything).Return(&entity.VersionRecord{}, nil) + suite.repo. + On("FindOne", mock.Anything). + Return(&entity.VersionRecord{Type: "migration", Version: 1, Description: "BasicTestDesc", Timestamp: time.Now()}, nil) + + migration_manager := NewMigrate(suite.repo) + migration_manager.Register(migrateHandler) + migration_manager.Up(1) + + migrateHandler.AssertNumberOfCalls(suite.T(), "Up", 1) + suite.repo.AssertNumberOfCalls(suite.T(), "Insert", 1) +} + +func (suite *MigrationManagerTestSuite) TestBasicTestNotUpgradeMigrationVersionWhenIsMinor() { + newMigrationMinorVersion := uint64(1) + currentMigrationMinorVersion := uint64(3) + + migrateHandler := &MigrationHandlerMock{} + migrateHandler.On("GetVersion").Return(newMigrationMinorVersion) + migrateHandler.On("GetName").Return("BasicTest") + migrateHandler.On("GetType").Return("migration") + migrateHandler.On("Up").Return(nil) + + suite.repo.On("CreateCollectionIfNotExists", mock.Anything).Return(nil) + suite.repo.On("Insert", mock.Anything).Return(&entity.VersionRecord{}, nil) + suite.repo. + On("FindOne", mock.Anything). + Return(&entity.VersionRecord{Type: "migration", Version: currentMigrationMinorVersion, Description: "BasicTestDesc", Timestamp: time.Now()}, nil) + + migration_manager := NewMigrate(suite.repo) + migration_manager.Register(migrateHandler) + migration_manager.Up(1) + + migrateHandler.AssertNumberOfCalls(suite.T(), "Up", 0) + suite.repo.AssertNumberOfCalls(suite.T(), "Insert", 0) +} + +func (suite *MigrationManagerTestSuite) TestBasicTestNotUpgradeMigrationVersionWhenHappensError() { + newMigrationMinorVersion := uint64(3) + currentMigrationMinorVersion := uint64(3) + + migrateHandler := &MigrationHandlerMock{} + migrateHandler.On("GetVersion").Return(newMigrationMinorVersion) + migrateHandler.On("GetName").Return("BasicTest") + migrateHandler.On("GetType").Return("migration") + migrateHandler.On("Up").Return(errors.New("force error on Up method.")) + + suite.repo.On("CreateCollectionIfNotExists", mock.Anything).Return(nil) + suite.repo.On("Insert", mock.Anything).Return(&entity.VersionRecord{}, nil) + suite.repo. + On("FindOne", mock.Anything). + Return(&entity.VersionRecord{Type: "migration", Version: currentMigrationMinorVersion, Description: "BasicTestDesc", Timestamp: time.Now()}, nil) + + migration_manager := NewMigrate(suite.repo) + migration_manager.Register(migrateHandler) + migration_manager.Up(1) + + migrateHandler.AssertNumberOfCalls(suite.T(), "Up", 0) + suite.repo.AssertNumberOfCalls(suite.T(), "Insert", 0) +} + +func (suite *MigrationManagerTestSuite) TestBasicTestNotUpgradeMigrationVersionWhenHappensErrorOnVersion() { + newMigrationMinorVersion := uint64(3) + currentMigrationMinorVersion := uint64(3) + + migrateHandler := &MigrationHandlerMock{} + migrateHandler.On("GetVersion").Return(newMigrationMinorVersion) + migrateHandler.On("GetName").Return("BasicTest") + migrateHandler.On("GetType").Return("migration") + migrateHandler.On("Up").Return(nil) + + suite.repo.On("CreateCollectionIfNotExists", mock.Anything).Return(errors.New("force error on CreateCollectionIfNotExists")) + suite.repo.On("Insert", mock.Anything).Return(&entity.VersionRecord{}, nil) + suite.repo. + On("FindOne", mock.Anything). + Return(&entity.VersionRecord{Type: "migration", Version: currentMigrationMinorVersion, Description: "BasicTestDesc", Timestamp: time.Now()}, nil) + + migration_manager := NewMigrate(suite.repo) + migration_manager.Register(migrateHandler) + migration_manager.Up(1) + + migrateHandler.AssertNumberOfCalls(suite.T(), "Up", 0) + suite.repo.AssertNumberOfCalls(suite.T(), "Insert", 0) +} + +func (suite *MigrationManagerTestSuite) TestBasicTestNotUpgradeMigrationVersionWhenHappensErrorOnUp() { + newMigrationMinorVersion := uint64(3) + currentMigrationMinorVersion := uint64(2) + + migrateHandler := &MigrationHandlerMock{} + migrateHandler.On("GetVersion").Return(newMigrationMinorVersion) + migrateHandler.On("GetName").Return("BasicTest") + migrateHandler.On("GetType").Return("migration") + migrateHandler.On("Up").Return(errors.New("force error on Up method.")) + + suite.repo.On("CreateCollectionIfNotExists", mock.Anything).Return(nil) + suite.repo.On("Insert", mock.Anything).Return(&entity.VersionRecord{}, nil) + suite.repo. + On("FindOne", mock.Anything). + Return(&entity.VersionRecord{Type: "migration", Version: currentMigrationMinorVersion, Description: "BasicTestDesc", Timestamp: time.Now()}, nil) + + migration_manager := NewMigrate(suite.repo) + migration_manager.Register(migrateHandler) + migration_manager.Up(1) + + migrateHandler.AssertNumberOfCalls(suite.T(), "Up", 1) + suite.repo.AssertNumberOfCalls(suite.T(), "Insert", 0) +} + +func (suite *MigrationManagerTestSuite) TestBasicTestNotUpgradeMigrationVersionWhenHappensErrorOnUp3() { + newMigrationMinorVersion := uint64(3) + currentMigrationMinorVersion := uint64(2) + + migrateHandler := &MigrationHandlerMock{} + migrateHandler.On("GetVersion").Return(newMigrationMinorVersion) + migrateHandler.On("GetName").Return("BasicTest") + migrateHandler.On("GetType").Return("migration") + migrateHandler.On("Up").Return(nil) + + suite.repo.On("CreateCollectionIfNotExists", mock.Anything).Return(nil) + insertError := errors.New("force error on Insert") + suite.repo.On("Insert", mock.Anything).Return(&entity.VersionRecord{}, insertError) + suite.repo. + On("FindOne", mock.Anything). + Return(&entity.VersionRecord{Type: "migration", Version: currentMigrationMinorVersion, Description: "BasicTestDesc", Timestamp: time.Now()}, nil) + + migration_manager := NewMigrate(suite.repo) + migration_manager.Register(migrateHandler) + upError := migration_manager.Up(1) + + migrateHandler.AssertNumberOfCalls(suite.T(), "Up", 1) + suite.repo.AssertNumberOfCalls(suite.T(), "Insert", 1) + suite.Equal(insertError.Error(), upError.Error()) +} + +func (suite *MigrationManagerTestSuite) TestBasicTestNotExecuteWhenThereIsNotMigrations() { + migrateHandler := &MigrationHandlerMock{} + + migration_manager := NewMigrate(suite.repo) + migration_manager.Up(0) + + migrateHandler.AssertNumberOfCalls(suite.T(), "Up", 0) + suite.repo.AssertNumberOfCalls(suite.T(), "Insert", 0) +} + +func TestMigrationManagerTestSuite(t *testing.T) { + suite.Run(t, new(MigrationManagerTestSuite)) +} diff --git a/src/pkg/migration.go b/src/pkg/migration.go new file mode 100644 index 0000000..bfe1e31 --- /dev/null +++ b/src/pkg/migration.go @@ -0,0 +1,9 @@ +package migrate + +import "github.com/iamviniciuss/golang-migrations/src/adapter" + +type Migration struct { + Version uint64 + Description string + Handler adapter.MigrationHandler +} diff --git a/src/pkg/mock.go b/src/pkg/mock.go new file mode 100644 index 0000000..e1ebc87 --- /dev/null +++ b/src/pkg/mock.go @@ -0,0 +1,54 @@ +package migrate + +import ( + entity "github.com/iamviniciuss/golang-migrations/src/dto" + "github.com/stretchr/testify/mock" +) + +type MigrationRepositoryMock struct { + mock.Mock +} + +func (m *MigrationRepositoryMock) Insert(rec *entity.VersionRecord) (*entity.VersionRecord, error) { + args := m.Called(rec) + return args.Get(0).(*entity.VersionRecord), args.Error(1) +} + +func (m *MigrationRepositoryMock) FindOne(rec *entity.VersionRecord) (*entity.VersionRecord, error) { + args := m.Called(rec) + return args.Get(0).(*entity.VersionRecord), args.Error(1) +} + +func (m *MigrationRepositoryMock) CreateCollectionIfNotExists(name string) error { + args := m.Called(name) + return args.Error(0) +} + +type MigrationHandlerMock struct { + mock.Mock +} + +func (m *MigrationHandlerMock) GetVersion() uint64 { + args := m.Called() + return args.Get(0).(uint64) +} + +func (m *MigrationHandlerMock) GetType() string { + args := m.Called() + return args.String(0) +} + +func (m *MigrationHandlerMock) GetName() string { + args := m.Called() + return args.String(0) +} + +func (m *MigrationHandlerMock) Up() error { + args := m.Called() + return args.Error(0) +} + +func (m *MigrationHandlerMock) Down() error { + args := m.Called() + return args.Error(0) +} diff --git a/src/pkg/mongo_connection.go b/src/pkg/mongo_connection.go new file mode 100644 index 0000000..35720d5 --- /dev/null +++ b/src/pkg/mongo_connection.go @@ -0,0 +1,29 @@ +package migrate + +import ( + "context" + "time" + + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +// export MONGO_URL=mongodb://localhost:27017 +func MongoConnect(databaseHost string, databaseName string) (*mongo.Database, error) { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + client, err := mongo.Connect(options.Client().ApplyURI(databaseHost)) + if err != nil { + return nil, err + } + + err = client.Ping(ctx, nil) + if err != nil { + return nil, err + } + + db := client.Database(databaseName) + + return db, nil +} diff --git a/src/pkg/utils.go b/src/pkg/utils.go new file mode 100644 index 0000000..7000f8c --- /dev/null +++ b/src/pkg/utils.go @@ -0,0 +1,22 @@ +package migrate + +import ( + "sort" + + repository "github.com/iamviniciuss/golang-migrations/src/adapter" +) + +func migrationSort(migrations []Migration) { + sort.Slice(migrations, func(i, j int) bool { + return migrations[i].Version < migrations[j].Version + }) +} + +func hasVersion(migrations []Migration, current repository.MigrationHandler) bool { + for _, m := range migrations { + if m.Description == current.GetName() && m.Version == current.GetVersion() { + return true + } + } + return false +} diff --git a/src/repository/migration_mongodb.go b/src/repository/migration_mongodb.go new file mode 100644 index 0000000..3887c99 --- /dev/null +++ b/src/repository/migration_mongodb.go @@ -0,0 +1,127 @@ +package repository + +import ( + "context" + + entity "github.com/iamviniciuss/golang-migrations/src/dto" + "github.com/pkg/errors" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +const defaultMigrationsCollection = "migrations" + +const AllAvailable = -1 + +type MigrationRepositoryMongo struct { + db *mongo.Database +} + +func NewMigrationRepositoryMongo(db *mongo.Database) *MigrationRepositoryMongo { + return &MigrationRepositoryMongo{db} +} + +func (erm *MigrationRepositoryMongo) Insert(rec *entity.VersionRecord) (*entity.VersionRecord, error) { + _, err := erm.db.Collection(defaultMigrationsCollection).InsertOne(context.TODO(), rec) + if err != nil { + return rec, err + } + + return rec, nil +} + +func (erm *MigrationRepositoryMongo) FindOne(reccord *entity.VersionRecord) (*entity.VersionRecord, error) { + + filter := bson.M{"version": reccord.Version, "description": reccord.Description, "type": reccord.Type} + sort := bson.D{bson.E{Key: "_id", Value: -1}} + options := options.FindOne().SetSort(sort) + + result := erm.db.Collection(defaultMigrationsCollection).FindOne(context.TODO(), filter, options) + err := result.Err() + switch { + case err == mongo.ErrNoDocuments: + return nil, err + case err != nil: + return nil, err + } + + var output entity.VersionRecord + if err := result.Decode(&output); err != nil { + return nil, err + } + + return &output, nil +} + +func (erm *MigrationRepositoryMongo) isCollectionExist(name string) (isExist bool, err error) { + collections, err := erm.getCollections() + if err != nil { + return false, err + } + + for _, c := range collections { + if name == c.Name { + return true, nil + } + } + return false, nil +} + +func (erm *MigrationRepositoryMongo) CreateCollectionIfNotExists(name string) error { + exist, err := erm.isCollectionExist(name) + if err != nil { + return err + } + if exist { + return nil + } + + command := bson.D{bson.E{Key: "create", Value: name}} + err = erm.db.RunCommand(context.TODO(), command).Err() + if err != nil { + return err + } + + return nil +} + +func (erm *MigrationRepositoryMongo) getCollections() (collections []entity.CollectionSpecification, err error) { + filter := bson.D{bson.E{Key: "type", Value: "collection"}} + options := options.ListCollections().SetNameOnly(true) + + cursor, err := erm.db.ListCollections(context.Background(), filter, options) + if err != nil { + return nil, err + } + + if cursor != nil { + defer func(cursor *mongo.Cursor) { + curErr := cursor.Close(context.TODO()) + if curErr != nil { + if err != nil { + err = errors.Wrapf(curErr, "migrate: get collection failed: %s", err.Error()) + } else { + err = curErr + } + } + }(cursor) + } + + for cursor.Next(context.TODO()) { + var collection entity.CollectionSpecification + + err := cursor.Decode(&collection) + if err != nil { + return nil, err + } + + collections = append(collections, collection) + } + + if err := cursor.Err(); err != nil { + return nil, err + } + + return +} diff --git a/src/repository/migration_mysql.go b/src/repository/migration_mysql.go new file mode 100644 index 0000000..cc8401d --- /dev/null +++ b/src/repository/migration_mysql.go @@ -0,0 +1,70 @@ +package repository + +import ( + "database/sql" + "fmt" + + _ "github.com/golang-migrate/migrate/v4/database/mysql" + _ "github.com/golang-migrate/migrate/v4/source/file" + entity "github.com/iamviniciuss/golang-migrations/src/dto" + "github.com/pkg/errors" +) + +const migrationsPath = "file://path/to/your/migrations" + +type MigrationRepositoryMySQL struct { + db *sql.DB + migrationTableName string +} + +func NewMigrationRepositoryMySQL(db *sql.DB, migrationTableName string) *MigrationRepositoryMySQL { + return &MigrationRepositoryMySQL{db, migrationTableName} +} + +func (erm *MigrationRepositoryMySQL) Insert(rec *entity.VersionRecord) (*entity.VersionRecord, error) { + _, err := erm.db.Exec("INSERT INTO migrations (version, description, type) VALUES (?, ?, ?)", rec.Version, rec.Description, rec.Type) + if err != nil { + return rec, err + } + return rec, nil +} + +func (erm *MigrationRepositoryMySQL) FindOne(record *entity.VersionRecord) (*entity.VersionRecord, error) { + var output entity.VersionRecord + err := erm.db.QueryRow("SELECT version, description, type FROM migrations WHERE version = ? AND description = ? AND type = ?", record.Version, record.Description, record.Type).Scan(&output.Version, &output.Description, &output.Type) + switch { + case errors.Is(err, sql.ErrNoRows): + return nil, nil + case err != nil: + return nil, err + } + return &output, nil +} + +func (erm *MigrationRepositoryMySQL) CreateCollectionIfNotExists(name string) error { + createTableQuery := fmt.Sprintf(` + CREATE TABLE IF NOT EXISTS %s ( + id INT AUTO_INCREMENT PRIMARY KEY, + nome VARCHAR(255), + idade INT + ); + `, erm.migrationTableName) + + _, err := erm.db.Exec(createTableQuery) + if err != nil { + return err + } + + fmt.Println("Tabela criada ou já existente.") + + return nil +} + +func NewConnection(hostname, username, password, dbName, tableName string, port int) (*sql.DB, error) { + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", username, password, hostname, port, dbName) + db, err := sql.Open("mysql", dsn) + if err != nil { + return nil, err + } + return db, nil +} diff --git a/src/repository/migration_repository.go b/src/repository/migration_repository.go new file mode 100644 index 0000000..c167dd1 --- /dev/null +++ b/src/repository/migration_repository.go @@ -0,0 +1,11 @@ +package repository + +import ( + entity "github.com/iamviniciuss/golang-migrations/src/dto" +) + +type MigrationRepository interface { + Insert(rec *entity.VersionRecord) (*entity.VersionRecord, error) + FindOne(rec *entity.VersionRecord) (*entity.VersionRecord, error) + CreateCollectionIfNotExists(name string) error +} diff --git a/tests/CreateMongoInMemory.go b/tests/CreateMongoInMemory.go new file mode 100644 index 0000000..46e9728 --- /dev/null +++ b/tests/CreateMongoInMemory.go @@ -0,0 +1,33 @@ +package mongodb + +import ( + "os" + "runtime" + + "github.com/tryvium-travels/memongo" + "github.com/tryvium-travels/memongo/memongolog" +) + +func CreateMongoTemp() *memongo.Server { + opts := &memongo.Options{ + MongoVersion: "5.0.5", + Logger: nil, + LogLevel: memongolog.LogLevel(5), + } + if runtime.GOARCH == "arm64" { + if runtime.GOOS == "darwin" { + // Only set the custom url as workaround for arm64 macs + opts.DownloadURL = "https://fastdl.mongodb.org/osx/mongodb-macos-x86_64-5.0.5.tgz" + } + } + mongoServer, err := memongo.StartWithOptions(opts) + + if err != nil { + panic(err) + } + + os.Setenv("MONGO_URL", mongoServer.URI()) + os.Setenv("DATABASE", memongo.RandomDatabase()) + + return mongoServer +} diff --git a/tests/seed/1_add-my-index-user.go b/tests/seed/1_add-my-index-user.go new file mode 100644 index 0000000..7a0494a --- /dev/null +++ b/tests/seed/1_add-my-index-user.go @@ -0,0 +1,40 @@ +package seed + +type addMyIndexUser struct { + typing string + name string + version uint64 + repository OnlineReviewRepository +} + +func NewAddMyIndexUser(repository OnlineReviewRepository) *addMyIndexUser { + return &addMyIndexUser{ + version: 1, + name: "addMyIndexUser", + typing: "seed", + repository: repository, + } +} + +func (ami *addMyIndexUser) GetName() string { + return ami.name +} + +func (ami *addMyIndexUser) GetType() string { + return ami.typing +} + +func (ami *addMyIndexUser) GetVersion() uint64 { + return ami.version +} + +func (ami *addMyIndexUser) Up() error { + ami.repository.Insert(&OnlineReview{ + Name: "Golang", + }) + return nil +} + +func (ami *addMyIndexUser) Down() error { + return nil +} diff --git a/tests/seed/1_add-my-index.go b/tests/seed/1_add-my-index.go new file mode 100644 index 0000000..02a31d5 --- /dev/null +++ b/tests/seed/1_add-my-index.go @@ -0,0 +1,65 @@ +package seed + +import ( + "context" + "fmt" + + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +type addMyIndex struct { + typing string + name string + version uint64 + db *mongo.Database +} + +func NewAddMyIndex(db *mongo.Database) *addMyIndex { + return &addMyIndex{ + version: 1, + name: "addMyIndex", + typing: "seed", + db: db, + //repositoryXXXX: XXXXX + } +} + +func (ami *addMyIndex) GetName() string { + return ami.name +} + +func (ami *addMyIndex) GetType() string { + return ami.typing +} + +func (ami *addMyIndex) GetVersion() uint64 { + return ami.version +} + +func (ami *addMyIndex) Up() error { + opt := options.Index().SetName("my-index") + keys := bson.D{{"my-key", 1}} + model := mongo.IndexModel{Keys: keys, Options: opt} + _, err := ami.db.Collection("my-coll").Indexes().CreateOne(context.TODO(), model) + if err != nil { + fmt.Println(err.Error()) + return err + } + + fmt.Println("Up:", ami.typing, ": ", ami.name, "executed with success") + + return nil +} + +func (ami *addMyIndex) Down() error { + err := ami.db.Collection("my-coll").Indexes().DropOne(context.TODO(), "my-index") + if err != nil { + fmt.Println(err.Error()) + return err + } + + fmt.Println("Down: ", ami.typing, ": ", ami.name, "executed with success") + return nil +} diff --git a/tests/seed/online_review_mongodb.go b/tests/seed/online_review_mongodb.go new file mode 100644 index 0000000..5d80c6b --- /dev/null +++ b/tests/seed/online_review_mongodb.go @@ -0,0 +1,35 @@ +package seed + +import ( + "context" + + "go.mongodb.org/mongo-driver/v2/mongo" +) + +type OnlineReviewRepository interface { + Insert(rec *OnlineReview) (*OnlineReview, error) +} + +type OnlineReview struct { + Id string `json:"_id"` + Name string `json:"name,omitempty"` +} + +const defaultOnlineCollection = "online_reviews" + +type OnlineRepositoryMongo struct { + db *mongo.Database +} + +func NewOnlineRepositoryMongo(db *mongo.Database) *OnlineRepositoryMongo { + return &OnlineRepositoryMongo{db} +} + +func (erm *OnlineRepositoryMongo) Insert(rec *OnlineReview) (*OnlineReview, error) { + _, err := erm.db.Collection(defaultOnlineCollection).InsertOne(context.TODO(), rec) + if err != nil { + return rec, err + } + + return rec, nil +} diff --git a/tests/suites/seed.go b/tests/suites/seed.go new file mode 100644 index 0000000..abeee54 --- /dev/null +++ b/tests/suites/seed.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + + // database "github.com/iamviniciuss/golang-migrations/src/database" + pkg "github.com/iamviniciuss/golang-migrations/src/pkg" + repository "github.com/iamviniciuss/golang-migrations/src/repository" + seeds "github.com/iamviniciuss/golang-migrations/tests/seed" +) + +func main() { + fmt.Println("Up seeds...") + + database, err := pkg.MongoConnect("mongodb://localhost:27017", "test-migration01") + + if err != nil { + panic(err) + } + + // mysqlconn, err := repository.NewConnection() + + // if err != nil { + // panic(err) + // } + + // defer mysqlconn.Close() + + // migrationRepo := repository.NewMigrationRepositoryMySQL(mysqlconn) + migrationRepo := repository.NewMigrationRepositoryMongo(database) + onlineReviewRepo := seeds.NewOnlineRepositoryMongo(database) + + migrationManager := pkg.NewMigrate(migrationRepo) + migrationManager.Register(seeds.NewAddMyIndexUser(onlineReviewRepo)) + migrationManager.Register(seeds.NewAddMyIndex(database)) + + if err := migrationManager.Up(pkg.AllAvailable); err != nil { + panic(err) + } +} diff --git a/util.go b/util.go deleted file mode 100644 index 3865150..0000000 --- a/util.go +++ /dev/null @@ -1,30 +0,0 @@ -package migrate - -import ( - "fmt" - "path/filepath" - "strconv" - "strings" -) - -func extractVersionDescription(name string) (uint64, string, error) { - base := filepath.Base(name) - - if ext := filepath.Ext(base); ext != ".go" { - return 0, "", fmt.Errorf("can not extract version from %q", base) - } - - idx := strings.IndexByte(base, '_') - if idx == -1 { - return 0, "", fmt.Errorf("can not extract version from %q", base) - } - - version, err := strconv.ParseUint(base[:idx], 10, 64) - if err != nil { - return 0, "", err - } - - description := base[idx+1 : len(base)-len(".go")] - - return version, description, nil -} diff --git a/util_test.go b/util_test.go deleted file mode 100644 index 8206421..0000000 --- a/util_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package migrate - -import "testing" - -func TestExtractVersionDescription(t *testing.T) { - version, description, err := extractVersionDescription("1_test.go") - if err != nil { - t.Error(err) - } - if version != 1 || description != "test" { - t.Errorf("Bad version/description: %v %v", version, description) - } - - _, _, err = extractVersionDescription("test.go") - if err == nil { - t.Errorf("Unexpected nil error") - } - - _, _, err = extractVersionDescription("test") - if err == nil { - t.Errorf("Unexpected nil error") - } - - _, _, err = extractVersionDescription("test_test.go") - if err == nil { - t.Errorf("Unexpected nil error") - } -}