diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9651b3a --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +DATABASE_URL="postgres://gouser:123456@localhost:5432/gotwitterclone?sslmode=disable" \ No newline at end of file diff --git a/.github/workflows/test-coverage-action.yml b/.github/workflows/test-coverage-action.__yml similarity index 100% rename from .github/workflows/test-coverage-action.yml rename to .github/workflows/test-coverage-action.__yml diff --git a/.gitignore b/.gitignore index 7a47eee..a84216c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -bin/api \ No newline at end of file +bin/api +/.env diff --git a/.testcoverage.yml b/.testcoverage.yml index d7a4d94..e9cf5fb 100644 --- a/.testcoverage.yml +++ b/.testcoverage.yml @@ -15,15 +15,15 @@ local-prefix: "github.com/org/project" threshold: # (optional; default 0) # The minimum coverage that each file should have - file: 60 + file: 5 # (optional; default 0) # The minimum coverage that each package should have - package: 70 + package: 5 # (optional; default 0) # The minimum total coverage project should have - total: 70 + total: 5 # Holds regexp rules which will override thresholds for matched files or packages # using their paths. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d03742c --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +The MIT License (MIT) + +Original Work +Copyright (c) 2016 Matthias Kadenbach +https://github.com/mattes/migrate + +Modified Work +Copyright (c) 2018 Dale Hui +https://github.com/golang-migrate/migrate + + +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/Makefile b/Makefile index a079ffa..8e67b54 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,10 @@ test: @go test ./... -v build: - @go build -o bin/api cmd/api/api.go + @go build -o bin/api cmd/api/main.go start: build ./bin/api + +seed: + @go run cmd/seeds/main.go \ No newline at end of file diff --git a/README.md b/README.md index 8738be2..3f10859 100644 --- a/README.md +++ b/README.md @@ -23,16 +23,16 @@ https://dbdiagram.io/d/go-twitter-clone-660b4e8437b7e33fd741027f // users data const users = [ { - id: '4cfe67a9-defc-42b9-8410-cb5086bec2f5', + id: '0194bd04-66e2-7cd8-b3d9-66eda709f2ee', username: 'alucard', }, { - id: 'b8903f77-5d16-4176-890f-f597594ff952', + id: '0194bd04-8eac-7e70-97cd-c526cdda3d6a', username: 'alexander', }, { - id: '75135a97-46be-405f-8948-0821290ca83e', + id: '0194bdb1-0588-7181-809e-a825badac714', username: 'seras_victoria', }, ]; @@ -54,6 +54,6 @@ ___ -## Styleguide +## Styleguide -[uber go style guide](https://github.com/alcir-junior-caju/uber-go-style-guide-pt-br) +[uber go style guide](https://github.com/alcir-junior-caju/uber-go-style-guide-pt-br) \ No newline at end of file diff --git a/adapter/input/adapter_http/posts_controller.go b/adapter/input/adapter_http/posts_controller.go index 6aa152d..23b2cc1 100644 --- a/adapter/input/adapter_http/posts_controller.go +++ b/adapter/input/adapter_http/posts_controller.go @@ -32,7 +32,7 @@ func (pc *postsController) CreatePost(c *gin.Context) { return } - postDomain, err := pc.createPostUseCase.Execute(input.CreatePostUseCaseInput{ + postDomain, err := pc.createPostUseCase.Execute(c, input.CreatePostUseCaseInput{ UserID: createPostRequest.UserID, Content: createPostRequest.Content, }) @@ -56,7 +56,7 @@ func (pc *postsController) CreateRepost(c *gin.Context) { return } - postDomain, err := pc.createRepostUseCase.Execute(input.CreateRepostUseCaseInput{ + postDomain, err := pc.createRepostUseCase.Execute(c, input.CreateRepostUseCaseInput{ PostID: createRequest.PostID, UserID: createRequest.UserID, }) @@ -80,7 +80,7 @@ func (pc *postsController) CreateQuote(c *gin.Context) { return } - postDomain, err := pc.createQuoteUseCase.Execute(input.CreateQuoteUseCaseInput{ + postDomain, err := pc.createQuoteUseCase.Execute(c, input.CreateQuoteUseCaseInput{ PostID: createRequest.PostID, UserID: createRequest.UserID, Quote: createRequest.Quote, diff --git a/adapter/input/adapter_http/users_controller.go b/adapter/input/adapter_http/users_controller.go index 407d1af..48aa405 100644 --- a/adapter/input/adapter_http/users_controller.go +++ b/adapter/input/adapter_http/users_controller.go @@ -31,7 +31,7 @@ func (uc *usersController) GetInfo(c *gin.Context) { c.JSON(errRest.Code, errRest) return } - userFeedDomain, err := uc.getUserInfoUseCase.Execute(userInfoRequest.Username) + userFeedDomain, err := uc.getUserInfoUseCase.Execute(c, userInfoRequest.Username) if err != nil { c.JSON(err.Code, err) return @@ -53,7 +53,7 @@ func (uc *usersController) GetFeed(c *gin.Context) { return } - userFeedDomain, err := uc.getUserFeedUseCase.Execute(userRequest.Username) + userFeedDomain, err := uc.getUserFeedUseCase.Execute(c, userRequest.Username) if err != nil { c.JSON(err.Code, err) return diff --git a/adapter/input/model/request/post_create_request.go b/adapter/input/model/request/post_create_request.go index 6c71e29..8fa1a0e 100644 --- a/adapter/input/model/request/post_create_request.go +++ b/adapter/input/model/request/post_create_request.go @@ -1,6 +1,6 @@ package request type CreatePostRequest struct { - UserID string `json:"user_id" binding:"required,uuid4"` + UserID string `json:"user_id" binding:"required,ulid"` Content string `json:"content" binding:"required,min=10,max=255"` } diff --git a/adapter/input/model/request/quote_request.go b/adapter/input/model/request/quote_request.go index 2efd38b..067fd0c 100644 --- a/adapter/input/model/request/quote_request.go +++ b/adapter/input/model/request/quote_request.go @@ -1,7 +1,7 @@ package request type QuoteRequest struct { - UserID string `json:"user_id" binding:"required,uuid4"` - PostID string `json:"post_id" binding:"required,uuid4"` + UserID string `json:"user_id" binding:"required,ulid"` + PostID string `json:"post_id" binding:"required,ulid"` Quote string `json:"quote" binding:"required,min=10,max=100"` } diff --git a/adapter/input/model/request/repost_request.go b/adapter/input/model/request/repost_request.go index d92ed7d..7568ffb 100644 --- a/adapter/input/model/request/repost_request.go +++ b/adapter/input/model/request/repost_request.go @@ -1,6 +1,6 @@ package request type RepostRequest struct { - UserID string `json:"user_id" binding:"required,uuid4"` - PostID string `json:"post_id" binding:"required,uuid4"` + UserID string `json:"user_id" binding:"required,ulid"` + PostID string `json:"post_id" binding:"required,ulid"` } diff --git a/adapter/input/model/request/user_request.go b/adapter/input/model/request/user_request.go index b9ae3db..3e88f33 100644 --- a/adapter/input/model/request/user_request.go +++ b/adapter/input/model/request/user_request.go @@ -1,7 +1,7 @@ package request type UserInfoRequest struct { - Username string `uri:"username" binding:"required,min=5,max=10,alphanum,lowercase"` + Username string `uri:"username" binding:"required,min=5,max=14,alphanum,lowercase"` } type UserFeedRequest struct { diff --git a/adapter/output/repository/inmemory/post_inmemory_repo.go b/adapter/output/repository/inmemory/post_inmemory_repo.go index 8d92bbe..f74a651 100644 --- a/adapter/output/repository/inmemory/post_inmemory_repo.go +++ b/adapter/output/repository/inmemory/post_inmemory_repo.go @@ -1,6 +1,7 @@ package inmemory import ( + "context" "errors" "github.com/dexfs/go-twitter-clone/adapter/output/mappers" inmemory_schema "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory/schema" @@ -22,7 +23,7 @@ func NewInMemoryPostRepository(db *database.InMemoryDB) *inMemoryPostRepository } } -func (r *inMemoryPostRepository) CreatePost(aPost *domain.Post) error { +func (r *inMemoryPostRepository) CreatePost(ctx context.Context, aPost *domain.Post) error { r.insert(&inmemory_schema.PostSchema{ ID: aPost.ID, UserID: aPost.User.ID, @@ -38,7 +39,7 @@ func (r *inMemoryPostRepository) CreatePost(aPost *domain.Post) error { return nil } -func (r *inMemoryPostRepository) HasReachedPostingLimitDay(aUserId string, aLimit uint64) bool { +func (r *inMemoryPostRepository) HasReachedPostingLimitDay(ctx context.Context, aUserId string, aLimit uint64) bool { var count = uint64(0) for _, currentData := range r.getAll() { @@ -57,7 +58,7 @@ func (r *inMemoryPostRepository) HasReachedPostingLimitDay(aUserId string, aLimi } } -func (r *inMemoryPostRepository) AllByUserID(aUser *domain.User) []*domain.Post { +func (r *inMemoryPostRepository) AllByUserID(ctx context.Context, aUser *domain.User) []*domain.Post { var feed []*domain.Post for _, currentData := range r.getAll() { if currentData.UserID == aUser.ID { @@ -68,7 +69,7 @@ func (r *inMemoryPostRepository) AllByUserID(aUser *domain.User) []*domain.Post return feed } -func (r *inMemoryPostRepository) FindByID(aPostID string) (*domain.Post, error) { +func (r *inMemoryPostRepository) FindByID(ctx context.Context, aPostID string) (*domain.Post, error) { for _, currentData := range r.getAll() { if currentData.ID == aPostID { var postUser *domain.User @@ -84,7 +85,7 @@ func (r *inMemoryPostRepository) FindByID(aPostID string) (*domain.Post, error) return nil, errors.New("post not found") } -func (r *inMemoryPostRepository) HasPostBeenRepostedByUser(postID string, userID string) bool { +func (r *inMemoryPostRepository) HasPostBeenRepostedByUser(ctx context.Context, postID string, userID string) bool { for _, vPost := range r.getAll() { if vPost.IsRepost { if vPost.UserID == userID && vPost.OriginalPostID == postID { diff --git a/adapter/output/repository/inmemory/user_inmemory_repo.go b/adapter/output/repository/inmemory/user_inmemory_repo.go index ecd0c60..9e0fc50 100644 --- a/adapter/output/repository/inmemory/user_inmemory_repo.go +++ b/adapter/output/repository/inmemory/user_inmemory_repo.go @@ -1,6 +1,7 @@ package inmemory import ( + "context" "errors" inmemory_schema "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory/schema" "github.com/dexfs/go-twitter-clone/internal/core/domain" @@ -19,7 +20,7 @@ func NewInMemoryUserRepository(db *database.InMemoryDB) *inMemoryUserRepository } } -func (r *inMemoryUserRepository) ByUsername(username string) (*domain.User, error) { +func (r *inMemoryUserRepository) ByUsername(ctx context.Context, username string) (*domain.User, error) { for _, currentUser := range r.getAll() { if currentUser.Username == username { return &domain.User{ @@ -34,7 +35,7 @@ func (r *inMemoryUserRepository) ByUsername(username string) (*domain.User, erro return nil, errors.New("user not found") } -func (r *inMemoryUserRepository) FindByID(id string) (*domain.User, error) { +func (r *inMemoryUserRepository) FindByID(ctx context.Context, id string) (*domain.User, error) { for _, currentUser := range r.getAll() { if currentUser.ID == id { return &domain.User{ diff --git a/adapter/output/repository/postgres/post_postgres_repo.go b/adapter/output/repository/postgres/post_postgres_repo.go new file mode 100644 index 0000000..46590ba --- /dev/null +++ b/adapter/output/repository/postgres/post_postgres_repo.go @@ -0,0 +1,184 @@ +package postgres + +import ( + "context" + "errors" + "fmt" + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "github.com/dexfs/go-twitter-clone/pkg/database" + "github.com/jackc/pgx/v5" +) + +type postgresPostRepository struct { + db *database.PostgresDB +} + +func NewPostgresPostRepository(db *database.PostgresDB) *postgresPostRepository { + return &postgresPostRepository{ + db: db, + } +} + +func (r *postgresPostRepository) CreatePost(ctx context.Context, aPost *domain.Post) error { + query := ` + INSERT INTO posts ( + post_id, user_id, content, is_quote, is_repost, + original_post_id, original_post_content, original_post_user_id, original_post_screen_name, created_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + ` + + return r.db.Insert(ctx, query, + aPost.ID, + aPost.User.ID, + aPost.Content, + aPost.IsQuote, + aPost.IsRepost, + aPost.OriginalPostID, + aPost.OriginalPostContent, + aPost.OriginalPostUserID, + aPost.OriginalPostScreenName, + aPost.CreatedAt, + ) +} + +func (r *postgresPostRepository) HasReachedPostingLimitDay(ctx context.Context, aUserId string, aLimit uint64) bool { + query := "select count(id) from posts where user_id = $1 AND DATE(created_at) = CURRENT_DATE" + row := r.db.FindOne(ctx, query, aUserId) + var count uint64 + + err := row.Scan(&count) + if err != nil { + return false + } + + reached := count >= aLimit + + if reached { + return true + } + + return false +} + +func (r *postgresPostRepository) HasPostBeenRepostedByUser(ctx context.Context, aPostID string, aUserID string) bool { + query := "select count(id) from posts where user_id = $1 AND original_post_id = $2 AND is_repost = true" + row := r.db.FindOne(ctx, query, aUserID, aPostID) + + var count uint64 + + err := row.Scan(&count) + if err != nil { + return false + } + + if count > 0 { + return true + } + + return false +} + +func (r *postgresPostRepository) AllByUserID(ctx context.Context, aUser *domain.User) []*domain.Post { + result := make([]*domain.Post, 0) + + query := ` + SELECT + posts.post_id, + posts.content, + posts.is_quote, + posts.is_repost, + posts.original_post_id, + posts.original_post_content, + posts.original_post_user_id, + posts.original_post_screen_name, + users.user_id, + users.username + FROM posts + INNER JOIN users ON posts.user_id = users.user_id + WHERE posts.user_id = $1` + + rows, err := r.db.Find(ctx, query, aUser.ID) + defer rows.Close() + + if err != nil { + return nil + } + + if !rows.Next() { + return nil + } + for rows.Next() { + var postSchema PostSchema + var userSchema UserSchema + if err := rows.Scan( + &postSchema.ID, + &postSchema.Content, + &postSchema.IsQuote, + &postSchema.IsRepost, + &postSchema.OriginalPostID, + &postSchema.OriginalPostContent, + &postSchema.OriginalPostUserID, + &postSchema.OriginalPostScreenName, + &userSchema.ID, + &userSchema.Username, + ); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil + } + return nil + } + user := userSchema.FromPersistence() + post := postSchema.FromPersistence(user) + result = append(result, post) + } + + return result +} + +func (r *postgresPostRepository) FindByID(ctx context.Context, aPostID string) (*domain.Post, error) { + query := ` + SELECT + posts.post_id, + posts.content, + posts.is_quote, + posts.is_repost, + posts.original_post_id, + posts.original_post_content, + posts.original_post_user_id, + posts.original_post_screen_name, + users.user_id, + users.username + FROM posts + INNER JOIN users ON posts.user_id = users.user_id + WHERE posts.post_id = $1` + + row := r.db.FindOne(ctx, query, aPostID) + + var postSchema PostSchema + var userSchema UserSchema + + if err := row.Scan( + &postSchema.ID, + &postSchema.Content, + &postSchema.IsQuote, + &postSchema.IsRepost, + &postSchema.OriginalPostID, + &postSchema.OriginalPostContent, + &postSchema.OriginalPostUserID, + &postSchema.OriginalPostScreenName, + &userSchema.ID, + &userSchema.Username, + ); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, errors.New("post not found") + } + return nil, fmt.Errorf("error scanning post: %w", err) + + } + + user := userSchema.FromPersistence() + post := postSchema.FromPersistence(user) + + return post, nil + +} diff --git a/adapter/output/repository/postgres/schema.go b/adapter/output/repository/postgres/schema.go new file mode 100644 index 0000000..a32bef6 --- /dev/null +++ b/adapter/output/repository/postgres/schema.go @@ -0,0 +1,50 @@ +package postgres + +import ( + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "time" +) + +type UserSchema struct { + ID string `db:"user_id"` + Username string `db:"username"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +func (u UserSchema) FromPersistence() *domain.User { + return &domain.User{ + ID: u.ID, + Username: u.Username, + CreatedAt: u.CreatedAt, + UpdatedAt: u.UpdatedAt, + } +} + +type PostSchema struct { + ID string `db:"post_id"` + UserID string `db:"user_id"` + Content string `db:"content"` + CreatedAt time.Time `db:"created_at"` + IsQuote bool `db:"is_quote"` + IsRepost bool `db:"is_repost"` + OriginalPostID string `db:"original_post_id"` + OriginalPostContent string `db:"original_post_content"` + OriginalPostUserID string `db:"original_post_user_id"` + OriginalPostScreenName string `db:"original_post_screen_name"` +} + +func (p *PostSchema) FromPersistence(aUser *domain.User) *domain.Post { + return &domain.Post{ + ID: p.ID, + User: aUser, + Content: p.Content, + CreatedAt: p.CreatedAt, + IsQuote: p.IsQuote, + IsRepost: p.IsRepost, + OriginalPostID: p.OriginalPostID, + OriginalPostContent: p.OriginalPostContent, + OriginalPostUserID: p.OriginalPostUserID, + OriginalPostScreenName: p.OriginalPostScreenName, + } +} diff --git a/adapter/output/repository/postgres/user_postgres_repo.go b/adapter/output/repository/postgres/user_postgres_repo.go new file mode 100644 index 0000000..2fbeb02 --- /dev/null +++ b/adapter/output/repository/postgres/user_postgres_repo.go @@ -0,0 +1,40 @@ +package postgres + +import ( + "context" + "errors" + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "github.com/dexfs/go-twitter-clone/pkg/database" +) + +type PostgresUserRepository struct { + db *database.PostgresDB +} + +func NewPostgresUserRepository(db *database.PostgresDB) *PostgresUserRepository { + return &PostgresUserRepository{ + db: db, + } +} + +func (r *PostgresUserRepository) ByUsername(ctx context.Context, username string) (*domain.User, error) { + query := "SELECT * FROM users WHERE username = $1" + row := r.db.FindOne(ctx, query, username) + + var user domain.User + if err := row.Scan(&user.ID, &user.Username, &user.CreatedAt, &user.UpdatedAt); err != nil { + return nil, errors.New("user not found") + } + return &user, nil +} + +func (r *PostgresUserRepository) FindByID(ctx context.Context, id string) (*domain.User, error) { + query := "SELECT * FROM users WHERE user_id = $1" + row := r.db.FindOne(ctx, query, id) + + var user domain.User + if err := row.Scan(&user.ID, &user.Username, &user.CreatedAt, &user.UpdatedAt); err != nil { + return nil, errors.New("user not found") + } + return &user, nil +} diff --git a/api/go-twitter-clone.http b/api/go-twitter-clone.http index 97f7b9f..8e947b2 100644 --- a/api/go-twitter-clone.http +++ b/api/go-twitter-clone.http @@ -9,7 +9,7 @@ Content-Type: application/json { "content": "Teste Post", - "user_id": "4cfe67a9-defc-42b9-8410-cb5086bec2f5" + "user_id": "01JJYY1S0JY0ERC1VQ3EEFNJC7" } ### @@ -19,7 +19,7 @@ Accept: application/json ### -GET http://localhost:8001/users/alucard/feed +GET http://localhost:8001/users/alexander/feed Accept: application/json ### diff --git a/cmd/api/main.go b/cmd/api/main.go index 3c483a7..0b258df 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -1,45 +1,53 @@ package main import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/routes" - "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory" - inmemory_schema "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory/schema" + "github.com/dexfs/go-twitter-clone/adapter/output/repository/postgres" "github.com/dexfs/go-twitter-clone/internal/core/port/output" "github.com/dexfs/go-twitter-clone/pkg/database" + "github.com/golang-migrate/migrate/v4" + _ "github.com/golang-migrate/migrate/v4/database/postgres" + _ "github.com/golang-migrate/migrate/v4/source/file" + "github.com/joho/godotenv" "log" - "time" + "os" ) var ( db *database.InMemoryDB + pgDB *database.PostgresDB userRepo output.UserPort postRepo output.PostPort ) func init() { + ctx := context.Background() + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + m, err := migrate.New("file://migrations", os.Getenv("DATABASE_URL")) + if err != nil { + log.Fatal("Error on load migrations:", err) + } + + if err := m.Up(); err != nil && err != migrate.ErrNoChange { + log.Fatal("Error on apply migrations:", err) + } + + log.Println("Migrations applied") + db = database.NewInMemoryDB() + pgDB = database.NewPostgresDB() + pgDB.Version(ctx) if db == nil { log.Fatal("database is nil") } - initialUsers := make([]*inmemory_schema.UserSchema, 0) - initialUsers = append(initialUsers, &inmemory_schema.UserSchema{ - ID: "4cfe67a9-defc-42b9-8410-cb5086bec2f5", - Username: "alucard", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - }) - initialUsers = append(initialUsers, &inmemory_schema.UserSchema{ - ID: "b8903f77-5d16-4176-890f-f597594ff952", - Username: "alexander", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - }) + userRepo = postgres.NewPostgresUserRepository(pgDB) + postRepo = postgres.NewPostgresPostRepository(pgDB) - db.RegisterSchema(inmemory.USER_SCHEMA_NAME, initialUsers) - db.RegisterSchema(inmemory.POST_SCHEMA_NAME, []*inmemory_schema.PostSchema{}) - userRepo = inmemory.NewInMemoryUserRepository(db) - postRepo = inmemory.NewInMemoryPostRepository(db) } type APIServer struct { @@ -60,6 +68,7 @@ func (s *APIServer) Run() error { func main() { log.Printf("Starting Application") server := NewAPIServer("8001") + defer pgDB.Close(context.Background()) if err := server.Run(); err != nil { log.Fatal("Error starting server:", err) diff --git a/cmd/api/main_test.go b/cmd/api/main_test.go index 9e0bd26..bb95d26 100644 --- a/cmd/api/main_test.go +++ b/cmd/api/main_test.go @@ -1,3 +1,6 @@ +//go:build ignore +// +build ignore + package main import ( diff --git a/cmd/seeds/main.go b/cmd/seeds/main.go new file mode 100644 index 0000000..724a22d --- /dev/null +++ b/cmd/seeds/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/dexfs/go-twitter-clone/pkg/seeders" + +func main() { + seeders.Run() +} diff --git a/docker-compose.yaml b/docker-compose.yaml index 6ec20b6..b1380a1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,57 @@ -version: "3" +version: '3.8' + services: api: build: . container_name: twitter_clone_api ports: - 3001:8001 + postgres: + image: postgres:15 + container_name: postgres + restart: always + environment: + POSTGRES_USER: gouser + POSTGRES_PASSWORD: 123456 + POSTGRES_DB: gotwitterclone + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + zookeeper: + image: confluentinc/cp-zookeeper:7.5.0 + container_name: zookeeper + restart: always + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ports: + - "2181:2181" + + kafka: + image: confluentinc/cp-kafka:7.5.0 + container_name: kafka + restart: always + depends_on: + - zookeeper + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + ports: + - "9092:9092" + + kafdrop: + image: obsidiandynamics/kafdrop:latest + container_name: kafdrop + restart: always + depends_on: + - kafka + environment: + KAFKA_BROKERCONNECT: kafka:9092 + ports: + - "9000:9000" + +volumes: + postgres_data: diff --git a/go.mod b/go.mod index 0f2efd9..9b3a506 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,12 @@ go 1.22.0 require ( github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 + github.com/gin-contrib/cors v1.7.2 github.com/gin-gonic/gin v1.9.1 github.com/go-playground/locales v0.14.1 github.com/go-playground/universal-translator v0.18.1 github.com/go-playground/validator/v10 v10.20.0 + github.com/golang-migrate/migrate/v4 v4.18.2 github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.9.0 ) @@ -19,24 +21,32 @@ require ( github.com/cloudwego/iasm v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/gin-contrib/cors v1.7.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.2 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/oklog/ulid/v2 v2.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/atomic v1.7.0 // indirect golang.org/x/arch v0.7.0 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.34.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 351c887..0b15565 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,11 @@ -github.com/bytedance/sonic v1.11.5 h1:G00FYjjqll5iQ1PYXynbg/hyzqBqavH8Mo9/oTopd9k= -github.com/bytedance/sonic v1.11.5/go.mod h1:X2PC2giUdj/Cv2lliWFLk6c/DUQok5rViJSemeB0wDw= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.0/go.mod h1:UmRT+IRTGKz/DAkzcEGzyVqQFJ7H9BqwBO3pm9H/+HY= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/cloudwego/base64x v0.1.3 h1:b5J/l8xolB7dyDTTmhJP2oTs5LdrjyrUFuNxdfq5hAg= -github.com/cloudwego/base64x v0.1.3/go.mod h1:1+1K5BUHIQzyapgpF7LwvOGAEDicKtt1umPV+aN8pi8= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= @@ -14,6 +13,18 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ 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.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8= +github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= +github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 h1:6VSn3hB5U5GeA6kQw4TwWIWbOhtvR2hmbBJnTOtqTWc= github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6/go.mod h1:YxOVT5+yHzKvwhsiSIWmbAYM3Dr9AEEbER2dVayfBkg= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= @@ -24,42 +35,84 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= -github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8= +github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -76,28 +129,36 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= -google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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/internal/core/domain/post.go b/internal/core/domain/post.go index d041701..5165fcf 100644 --- a/internal/core/domain/post.go +++ b/internal/core/domain/post.go @@ -3,6 +3,7 @@ package domain import ( "errors" "github.com/google/uuid" + "github.com/oklog/ulid/v2" "time" ) @@ -40,7 +41,7 @@ func NewPost(aNewPost NewPostInput) (*Post, error) { } return &Post{ - ID: uuid.NewString(), + ID: ulid.Make().String(), User: aNewPost.User, Content: aNewPost.Content, CreatedAt: time.Now(), @@ -63,7 +64,7 @@ func NewRepost(aRepostInput NewRepostQuoteInput) (*Post, error) { } return &Post{ - ID: uuid.NewString(), + ID: ulid.Make().String(), User: aRepostInput.User, CreatedAt: time.Now(), IsQuote: false, diff --git a/internal/core/port/input/create_quote_usecase.go b/internal/core/port/input/create_quote_usecase.go index 6503dd8..f2c9f38 100644 --- a/internal/core/port/input/create_quote_usecase.go +++ b/internal/core/port/input/create_quote_usecase.go @@ -1,12 +1,13 @@ package input import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" ) type CreateQuoteUseCase interface { - Execute(anInput CreateQuoteUseCaseInput) (*domain.Post, *rest_errors.RestError) + Execute(ctx context.Context, anInput CreateQuoteUseCaseInput) (*domain.Post, *rest_errors.RestError) } type CreateQuoteUseCaseInput struct { PostID string diff --git a/internal/core/port/input/create_repost_usecase.go b/internal/core/port/input/create_repost_usecase.go index a5c4de0..8fe5954 100644 --- a/internal/core/port/input/create_repost_usecase.go +++ b/internal/core/port/input/create_repost_usecase.go @@ -1,6 +1,7 @@ package input import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" ) @@ -11,5 +12,5 @@ type CreateRepostUseCaseInput struct { } type CreateRepostUseCase interface { - Execute(input CreateRepostUseCaseInput) (*domain.Post, *rest_errors.RestError) + Execute(ctx context.Context, input CreateRepostUseCaseInput) (*domain.Post, *rest_errors.RestError) } diff --git a/internal/core/port/input/createpost_usecase.go b/internal/core/port/input/createpost_usecase.go index 7e810a7..3fa6055 100644 --- a/internal/core/port/input/createpost_usecase.go +++ b/internal/core/port/input/createpost_usecase.go @@ -1,6 +1,7 @@ package input import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" ) @@ -11,5 +12,5 @@ type CreatePostUseCaseInput struct { } type CreatePostUseCase interface { - Execute(aInput CreatePostUseCaseInput) (*domain.Post, *rest_errors.RestError) + Execute(ctx context.Context, aInput CreatePostUseCaseInput) (*domain.Post, *rest_errors.RestError) } diff --git a/internal/core/port/input/getinfo_usecase.go b/internal/core/port/input/getinfo_usecase.go index 9608941..387906b 100644 --- a/internal/core/port/input/getinfo_usecase.go +++ b/internal/core/port/input/getinfo_usecase.go @@ -1,10 +1,11 @@ package input import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" ) type GetUserInfoUseCase interface { - Execute(username string) (*domain.User, *rest_errors.RestError) + Execute(ctx context.Context, username string) (*domain.User, *rest_errors.RestError) } diff --git a/internal/core/port/input/getuserfeed_usecase.go b/internal/core/port/input/getuserfeed_usecase.go index a3ead52..b2db3bd 100644 --- a/internal/core/port/input/getuserfeed_usecase.go +++ b/internal/core/port/input/getuserfeed_usecase.go @@ -1,10 +1,11 @@ package input import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" ) type GetUserFeedUseCase interface { - Execute(username string) ([]*domain.Post, *rest_errors.RestError) + Execute(ctx context.Context, username string) ([]*domain.Post, *rest_errors.RestError) } diff --git a/internal/core/port/output/post_port.go b/internal/core/port/output/post_port.go index ba0954f..3840ede 100644 --- a/internal/core/port/output/post_port.go +++ b/internal/core/port/output/post_port.go @@ -1,11 +1,14 @@ package output -import "github.com/dexfs/go-twitter-clone/internal/core/domain" +import ( + "context" + "github.com/dexfs/go-twitter-clone/internal/core/domain" +) type PostPort interface { - CreatePost(aPost *domain.Post) error - HasReachedPostingLimitDay(aUserId string, aLimit uint64) bool - HasPostBeenRepostedByUser(postID string, userID string) bool - AllByUserID(aUser *domain.User) []*domain.Post - FindByID(aPostID string) (*domain.Post, error) + CreatePost(ctx context.Context, aPost *domain.Post) error + HasReachedPostingLimitDay(ctx context.Context, aUserId string, aLimit uint64) bool + HasPostBeenRepostedByUser(ctx context.Context, aPostID string, aUserID string) bool + AllByUserID(ctx context.Context, aUser *domain.User) []*domain.Post + FindByID(ctx context.Context, aPostID string) (*domain.Post, error) } diff --git a/internal/core/port/output/user_port.go b/internal/core/port/output/user_port.go index 032ba54..a13119f 100644 --- a/internal/core/port/output/user_port.go +++ b/internal/core/port/output/user_port.go @@ -1,8 +1,11 @@ package output -import "github.com/dexfs/go-twitter-clone/internal/core/domain" +import ( + "context" + "github.com/dexfs/go-twitter-clone/internal/core/domain" +) type UserPort interface { - ByUsername(username string) (*domain.User, error) - FindByID(id string) (*domain.User, error) + ByUsername(ctx context.Context, username string) (*domain.User, error) + FindByID(ctx context.Context, id string) (*domain.User, error) } diff --git a/internal/core/usecase/create_quote_usecase.go b/internal/core/usecase/create_quote_usecase.go index ed2fcf9..794c31c 100644 --- a/internal/core/usecase/create_quote_usecase.go +++ b/internal/core/usecase/create_quote_usecase.go @@ -1,6 +1,7 @@ package usecase import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" "github.com/dexfs/go-twitter-clone/internal/core/port/input" @@ -23,13 +24,13 @@ func NewCreateQuoteUseCase(postPort output.PostPort, userPort output.UserPort) ( }, nil } -func (uc *createQuoteUseCase) Execute(anInput input.CreateQuoteUseCaseInput) (*domain.Post, *rest_errors.RestError) { - user, err := uc.userPort.FindByID(anInput.UserID) +func (uc *createQuoteUseCase) Execute(ctx context.Context, anInput input.CreateQuoteUseCaseInput) (*domain.Post, *rest_errors.RestError) { + user, err := uc.userPort.FindByID(ctx, anInput.UserID) if err != nil { return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) } - post, err := uc.postPort.FindByID(anInput.PostID) + post, err := uc.postPort.FindByID(ctx, anInput.PostID) if err != nil { return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) } @@ -44,7 +45,11 @@ func (uc *createQuoteUseCase) Execute(anInput input.CreateQuoteUseCaseInput) (*d return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) } - uc.postPort.CreatePost(newQuotePost) + err = uc.postPort.CreatePost(ctx, newQuotePost) + + if err != nil { + return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) + } return newQuotePost, nil } diff --git a/internal/core/usecase/createpost_usecase.go b/internal/core/usecase/createpost_usecase.go index e8829d4..1be1ca9 100644 --- a/internal/core/usecase/createpost_usecase.go +++ b/internal/core/usecase/createpost_usecase.go @@ -1,6 +1,7 @@ package usecase import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" "github.com/dexfs/go-twitter-clone/internal/core/port/input" @@ -20,13 +21,13 @@ func NewCreatePostUseCase(postPort output.PostPort, userPort output.UserPort) (* return &createPostUseCase{postPort: postPort, userPort: userPort}, nil } -func (uc *createPostUseCase) Execute(aInput input.CreatePostUseCaseInput) (*domain.Post, *rest_errors.RestError) { - hasReachedLimit := uc.postPort.HasReachedPostingLimitDay(aInput.UserID, 5) // @TODO mudar isso para vir das configurações +func (uc *createPostUseCase) Execute(ctx context.Context, aInput input.CreatePostUseCaseInput) (*domain.Post, *rest_errors.RestError) { + hasReachedLimit := uc.postPort.HasReachedPostingLimitDay(ctx, aInput.UserID, 5) // @TODO mudar isso para vir das configurações if hasReachedLimit { return &domain.Post{}, rest_errors.NewBadRequestError("you reached your posts day limit") } - user, err := uc.userPort.FindByID(aInput.UserID) + user, err := uc.userPort.FindByID(ctx, aInput.UserID) if err != nil { return &domain.Post{}, rest_errors.NewNotFoundError(err.Error()) } @@ -39,7 +40,10 @@ func (uc *createPostUseCase) Execute(aInput input.CreatePostUseCaseInput) (*doma return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) } - uc.postPort.CreatePost(aNewPost) + err = uc.postPort.CreatePost(ctx, aNewPost) + if err != nil { + return &domain.Post{}, rest_errors.NewInternalServerError(err.Error()) + } return aNewPost, nil } diff --git a/internal/core/usecase/createrepost_usecase.go b/internal/core/usecase/createrepost_usecase.go index aed4655..b8e76dc 100644 --- a/internal/core/usecase/createrepost_usecase.go +++ b/internal/core/usecase/createrepost_usecase.go @@ -1,6 +1,7 @@ package usecase import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" "github.com/dexfs/go-twitter-clone/internal/core/port/input" @@ -20,19 +21,19 @@ func NewCreateRepostUseCase(postPort output.PostPort, userPort output.UserPort) return &createRepostUseCase{postPort: postPort, userPort: userPort}, nil } -func (uc *createRepostUseCase) Execute(aInput input.CreateRepostUseCaseInput) (*domain.Post, *rest_errors.RestError) { - user, err := uc.userPort.FindByID(aInput.UserID) +func (uc *createRepostUseCase) Execute(ctx context.Context, aInput input.CreateRepostUseCaseInput) (*domain.Post, *rest_errors.RestError) { + user, err := uc.userPort.FindByID(ctx, aInput.UserID) if err != nil { return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) } - isReposted := uc.postPort.HasPostBeenRepostedByUser(aInput.PostID, aInput.UserID) + isReposted := uc.postPort.HasPostBeenRepostedByUser(ctx, aInput.PostID, aInput.UserID) if isReposted { return &domain.Post{}, rest_errors.NewBadRequestError("it is not possible repost a repost post") } - post, err := uc.postPort.FindByID(aInput.PostID) + post, err := uc.postPort.FindByID(ctx, aInput.PostID) if err != nil { return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) } @@ -48,7 +49,7 @@ func (uc *createRepostUseCase) Execute(aInput input.CreateRepostUseCaseInput) (* return &domain.Post{}, rest_errors.NewInternalServerError(err.Error()) } - uc.postPort.CreatePost(newRepost) + uc.postPort.CreatePost(ctx, newRepost) return newRepost, nil } diff --git a/internal/core/usecase/getuserfeed_usecase.go b/internal/core/usecase/getuserfeed_usecase.go index efbafaa..933c832 100644 --- a/internal/core/usecase/getuserfeed_usecase.go +++ b/internal/core/usecase/getuserfeed_usecase.go @@ -1,6 +1,7 @@ package usecase import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" "github.com/dexfs/go-twitter-clone/internal/core/port/output" @@ -22,12 +23,12 @@ func NewGetUserFeedUseCase(userPort output.UserPort, postPort output.PostPort) ( }, nil } -func (uc *getUserFeedUseCase) Execute(username string) ([]*domain.Post, *rest_errors.RestError) { - user, err := uc.userPort.ByUsername(username) +func (uc *getUserFeedUseCase) Execute(ctx context.Context, username string) ([]*domain.Post, *rest_errors.RestError) { + user, err := uc.userPort.ByUsername(ctx, username) if err != nil { return []*domain.Post{}, rest_errors.NewNotFoundError(err.Error()) } - posts := uc.postPort.AllByUserID(user) + posts := uc.postPort.AllByUserID(ctx, user) return posts, nil } diff --git a/internal/core/usecase/getuserinfo_usecase.go b/internal/core/usecase/getuserinfo_usecase.go index ea8f27b..522adf8 100644 --- a/internal/core/usecase/getuserinfo_usecase.go +++ b/internal/core/usecase/getuserinfo_usecase.go @@ -1,6 +1,7 @@ package usecase import ( + "context" "fmt" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" @@ -20,9 +21,9 @@ func NewGetUserInfoUseCase(userPort output.UserPort) (*getUserInfoUseCase, *rest }, nil } -func (s *getUserInfoUseCase) Execute(username string) (*domain.User, *rest_errors.RestError) { +func (s *getUserInfoUseCase) Execute(ctx context.Context, username string) (*domain.User, *rest_errors.RestError) { fmt.Sprintf("GetUserInfoService_Execute(%s)", username) - userInfoResponse, err := s.userPort.ByUsername(username) + userInfoResponse, err := s.userPort.ByUsername(ctx, username) if err != nil { return nil, rest_errors.NewNotFoundError(err.Error()) } diff --git a/migrations/000001_create_users_table.down.sql b/migrations/000001_create_users_table.down.sql new file mode 100644 index 0000000..93ad809 --- /dev/null +++ b/migrations/000001_create_users_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS users CASCADE; \ No newline at end of file diff --git a/migrations/000001_create_users_table.up.sql b/migrations/000001_create_users_table.up.sql new file mode 100644 index 0000000..33ea57a --- /dev/null +++ b/migrations/000001_create_users_table.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE users +( + user_id VARCHAR(36) PRIMARY KEY, -- Sem default, deve ser fornecido manualmente, ulid : ex: 01JJYY0V9AMD9656HT4BSV0ZEK + username varchar(50) NOT NULL UNIQUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); diff --git a/migrations/000002_create_posts_table.down.sql b/migrations/000002_create_posts_table.down.sql new file mode 100644 index 0000000..181b11a --- /dev/null +++ b/migrations/000002_create_posts_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS posts CASCADE; \ No newline at end of file diff --git a/migrations/000002_create_posts_table.up.sql b/migrations/000002_create_posts_table.up.sql new file mode 100644 index 0000000..51bcc7f --- /dev/null +++ b/migrations/000002_create_posts_table.up.sql @@ -0,0 +1,17 @@ +CREATE TABLE posts +( + post_id VARCHAR(36) PRIMARY KEY, -- Sem default, deve ser fornecido manualmente, ulid: ex: 01JJYY0V9AMD9656HT4BSV0ZEK + user_id VARCHAR(36) NOT NULL REFERENCES users (user_id) ON DELETE CASCADE, + content TEXT NOT NULL, + is_quote BOOLEAN NOT NULL DEFAULT FALSE, + is_repost BOOLEAN NOT NULL DEFAULT FALSE, + original_post_id VARCHAR(36) NULL, + original_post_content TEXT NULL, + original_post_user_id VARCHAR(36) NULL, + original_post_screen_name TEXT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +-- CONSTRAINT fk_original_post FOREIGN KEY (original_post_id) REFERENCES posts (id) ON DELETE SET NULL +); + +CREATE INDEX idx_posts_user_created_at ON posts (user_id, created_at DESC); +CREATE INDEX idx_posts_original_post_id ON posts (original_post_id); \ No newline at end of file diff --git a/pkg/database/postgres_db.go b/pkg/database/postgres_db.go new file mode 100644 index 0000000..90443cb --- /dev/null +++ b/pkg/database/postgres_db.go @@ -0,0 +1,77 @@ +package database + +import ( + "context" + "errors" + "fmt" + "github.com/jackc/pgx/v5" + "log" + "os" + "time" +) + +type PostgresDB struct { + conn *pgx.Conn +} + +func NewPostgresDB() *PostgresDB { + conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL")) + if err != nil { + log.Fatal("Error on connect database", err) + } + + return &PostgresDB{ + conn: conn, + } +} + +func (db *PostgresDB) FindOne(ctx context.Context, query string, args ...any) pgx.Row { + return db.conn.QueryRow(ctx, query, args...) +} + +func (db *PostgresDB) Find(ctx context.Context, query string, args ...any) (pgx.Rows, error) { + return db.conn.Query(ctx, query, args...) +} + +func (db *PostgresDB) Batch(ctx context.Context, batch *pgx.Batch, dataSize int) error { + sendStart := time.Now() + br := db.conn.SendBatch(ctx, batch) + sendDuration := time.Since(sendStart) + + defer br.Close() + for i := 0; i < dataSize; i++ { + queryStart := time.Now() + _, err := br.Exec() + if err != nil { + return errors.New("User seed failed: " + err.Error()) + } + queryDuration := time.Since(queryStart) + fmt.Printf("Query %d: %v\n", i+1, queryDuration) + } + + fmt.Printf("Tempo de envio do batch: %v\n", sendDuration) + fmt.Printf("Tempo total: %v\n", time.Since(sendStart)) + return nil +} + +func (db *PostgresDB) Insert(ctx context.Context, query string, args ...any) error { + _, err := db.conn.Exec(ctx, query, args...) + if err != nil { + return err + } + + return nil +} + +func (db *PostgresDB) Close(ctx context.Context) { + db.conn.Close(ctx) +} + +func (db *PostgresDB) Version(ctx context.Context) { + var version string + err := db.conn.QueryRow(ctx, "SELECT version()").Scan(&version) + if err != nil { + log.Fatal("Erro ao executar query:", err) + } + fmt.Println("PostgreSQL version:", version) +} diff --git a/pkg/seeders/postgres_seed.go b/pkg/seeders/postgres_seed.go new file mode 100644 index 0000000..bcbedb5 --- /dev/null +++ b/pkg/seeders/postgres_seed.go @@ -0,0 +1,128 @@ +package seeders + +import ( + "context" + "github.com/dexfs/go-twitter-clone/adapter/output/repository/postgres" + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "github.com/dexfs/go-twitter-clone/pkg/database" + "github.com/jackc/pgx/v5" + "strconv" + "time" +) + +type PostgresSeeder struct { + db *database.PostgresDB +} + +func NewPostgresSeed(db *database.PostgresDB) *PostgresSeeder { + return &PostgresSeeder{db: db} +} + +func (s *PostgresSeeder) UsersSeed(ctx context.Context) error { + batch := &pgx.Batch{} + initialUsers := make([]*postgres.UserSchema, 0) + initialUsers = append(initialUsers, &postgres.UserSchema{ + ID: "01JJYY0V9AMD9656HT4BSV0ZEK", + Username: "alucard", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + + initialUsers = append(initialUsers, &postgres.UserSchema{ + ID: "01JJYY1S0JY0ERC1VQ3EEFNJC7", + Username: "alexander", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + + initialUsers = append(initialUsers, &postgres.UserSchema{ + ID: "01JJYY1Z0E3BMZQ0HFDH8A6NMT", + Username: "seras_victoria", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + + batch.Queue("DELETE FROM users") + for _, u := range initialUsers { + batch.Queue(`INSERT INTO users (user_id, username, created_at, updated_at) VALUES ($1, $2, $3, $4)`, + u.ID, u.Username, u.CreatedAt, u.UpdatedAt) + } + + return s.db.Batch(ctx, batch, len(initialUsers)) +} + +func (s *PostgresSeeder) PostsSeed(ctx context.Context) error { + batch := &pgx.Batch{} + + users := make([]*domain.User, 0) + users = append(users, &domain.User{ + ID: "01JJYY0V9AMD9656HT4BSV0ZEK", + Username: "alucard", + }) + users = append(users, &domain.User{ + ID: "01JJYY1S0JY0ERC1VQ3EEFNJC7", + Username: "alexander", + }) + batch.Queue("DELETE FROM posts") + // Criar 3 posts por user + // - alucard criar postatgem + queryInsertPost := ` + INSERT INTO posts ( + post_id, user_id, content, is_quote, is_repost, + original_post_id, original_post_content, original_post_user_id, original_post_screen_name, created_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + ` + var userRepost *domain.User + for _, user := range users { + for i := 0; i < 3; i++ { + aPost, _ := domain.NewPost(domain.NewPostInput{User: user, Content: user.Username + " post " + strconv.Itoa(i) + " from seed"}) + batch.Queue(queryInsertPost, aPost.ID, + aPost.User.ID, + aPost.Content, + aPost.IsQuote, + aPost.IsRepost, + aPost.OriginalPostID, + aPost.OriginalPostContent, + aPost.OriginalPostUserID, + aPost.OriginalPostScreenName, + aPost.CreatedAt) + + if user.Username == "alucard" { + userRepost = users[1] + } else { + userRepost = users[0] + } + + aRepost, _ := domain.NewRepost(domain.NewRepostQuoteInput{User: userRepost, Post: aPost}) + batch.Queue(queryInsertPost, aRepost.ID, + aRepost.User.ID, + aRepost.Content, + aRepost.IsQuote, + aRepost.IsRepost, + aRepost.OriginalPostID, + aRepost.OriginalPostContent, + aRepost.OriginalPostUserID, + aRepost.OriginalPostScreenName, + aRepost.CreatedAt) + + aQuote, _ := domain.NewQuote(domain.NewRepostQuoteInput{User: userRepost, Post: aPost, Content: userRepost.Username + " quote " + strconv.Itoa(i)}) + batch.Queue(queryInsertPost, aQuote.ID, + aQuote.User.ID, + aQuote.Content, + aQuote.IsQuote, + aQuote.IsRepost, + aQuote.OriginalPostID, + aQuote.OriginalPostContent, + aQuote.OriginalPostUserID, + aQuote.OriginalPostScreenName, + aQuote.CreatedAt) + + } + } + err := s.db.Batch(ctx, batch, len(users)) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/seeders/seeds.go b/pkg/seeders/seeds.go new file mode 100644 index 0000000..e51b7bc --- /dev/null +++ b/pkg/seeders/seeds.go @@ -0,0 +1,31 @@ +package seeders + +import ( + "context" + "github.com/dexfs/go-twitter-clone/pkg/database" + "github.com/joho/godotenv" + "log" + "time" +) + +func Run() { + ctx := context.Background() + err := godotenv.Load() + pgDB := database.NewPostgresDB() + defer pgDB.Close(ctx) + pgSeeder := NewPostgresSeed(pgDB) + ctxWithCancel, cancel := context.WithTimeout(ctx, 20*time.Second) + defer cancel() + + err = pgSeeder.UsersSeed(ctxWithCancel) + if err != nil { + log.Fatal("Error on seeding users: ", err) + } + + err = pgSeeder.PostsSeed(ctxWithCancel) + if err != nil { + log.Fatal("Error on seeding posts: ", err) + } + + log.Println("seeds applied") +}