From 44956e86b23293e1fca4188597af5b050b01d4bf Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 24 Jan 2026 14:14:32 -0300 Subject: [PATCH] feat: migrate all ID fields to UUIDv7 Replace auto-incrementing integer primary keys with UUIDv7 across the entire application for improved scalability, global uniqueness, and time-ordered sorting capabilities. Changes: Database Migrations: - PostgreSQL: Change SERIAL to UUID type for all primary keys - MySQL: Change BIGINT AUTO_INCREMENT to BINARY(16) for all primary keys - Update both users and outbox_events table migrations Domain Models: - user.User: Change ID field from int64 to uuid.UUID - outbox.OutboxEvent: Change ID field from int64 to uuid.UUID - user/http/dto.UserResponse: Update ID field to uuid.UUID Application Code: - Generate UUIDs in application using uuid.NewV7() instead of database auto-increment - User creation: Generate UUID before database insertion - OutboxEvent creation: Generate UUID before database insertion - Update all repository method signatures to accept uuid.UUID Repository Layer: - user/repository: GetByID() now accepts uuid.UUID parameter - All repository implementations compatible with UUID storage Use Case Layer: - user/usecase: Update interfaces and implementations to use uuid.UUID - GetUserByID() signature changed to accept uuid.UUID Tests: - Update all test files to use uuid.Must(uuid.NewV7()) - Update mock implementations for UUID compatibility - worker/event_worker_test: Use UUIDs in test fixtures - user/repository_test: Use UUIDs in test cases - user/usecase_test: Update mock methods and assertions - http/http_test: Update handler tests for UUID responses Dependencies: - Add github.com/google/uuid v1.6.0 Documentation: - Add comprehensive UUIDv7 section to README.md - Document benefits: time-ordering, global uniqueness, scalability - Provide implementation examples for new domains - Update code examples throughout README Benefits: - Time-ordered IDs maintain temporal sorting - Globally unique across distributed systems - Better database index performance than random UUIDs - No centralized ID generation required - Merge-friendly for database consolidation scenarios Breaking Changes: - All ID fields changed from int64 to uuid.UUID - API responses now return UUIDs as strings - Database schema requires migration --- README.md | 69 ++++++++++++++++++- go.mod | 1 + go.sum | 2 + internal/http/http_test.go | 8 ++- internal/outbox/domain/outbox_event.go | 8 ++- .../repository/outbox_repository_test.go | 13 ++-- internal/user/domain/user.go | 3 +- internal/user/http/dto/response.go | 8 ++- internal/user/repository/user_repository.go | 3 +- .../user/repository/user_repository_test.go | 16 +++-- internal/user/usecase/user_usecase.go | 9 ++- internal/user/usecase/user_usecase_test.go | 20 +++--- internal/worker/event_worker.go | 4 +- internal/worker/event_worker_test.go | 29 +++++--- .../mysql/000001_create_users_table.up.sql | 2 +- .../000002_create_outbox_events_table.up.sql | 2 +- .../000001_create_users_table.up.sql | 2 +- .../000002_create_outbox_events_table.up.sql | 2 +- 18 files changed, 153 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 79d3d31..caf6cea 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A production-ready Go project template following Clean Architecture and Domain-D - **Dependency Injection Container** - Centralized component wiring with lazy initialization and clean resource management - **Multiple Database Support** - PostgreSQL and MySQL via unified repository layer - **Database Migrations** - Separate migrations for PostgreSQL and MySQL using golang-migrate +- **UUIDv7 Primary Keys** - Time-ordered, sortable UUIDs for globally unique identifiers - **Transaction Management** - TxManager interface for handling database transactions - **Transactional Outbox Pattern** - Event-driven architecture with guaranteed delivery - **HTTP Server** - Standard library HTTP server with middleware for logging and panic recovery @@ -172,6 +173,68 @@ formatters: This ensures the linter correctly groups your local imports. +### UUIDv7 Primary Keys + +The project uses **UUIDv7** for all primary keys instead of auto-incrementing integers. UUIDv7 provides several advantages: + +**Benefits:** +- **Time-ordered**: UUIDs include timestamp information, maintaining temporal ordering +- **Globally unique**: No collision risk across distributed systems or databases +- **Database friendly**: Better index performance than random UUIDs (v4) due to sequential nature +- **Scalability**: No need for centralized ID generation or coordination +- **Merge-friendly**: Databases can be merged without ID conflicts + +**Implementation:** + +All ID fields use `uuid.UUID` type from `github.com/google/uuid`: + +```go +import "github.com/google/uuid" + +type User struct { + ID uuid.UUID `db:"id" json:"id"` + Name string `db:"name"` + Email string `db:"email"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} +``` + +IDs are generated in the application code using `uuid.NewV7()`: + +```go +user := &domain.User{ + ID: uuid.Must(uuid.NewV7()), + Name: input.Name, + Email: input.Email, + Password: hashedPassword, +} +``` + +**Database Storage:** +- **PostgreSQL**: `UUID` type (native support) +- **MySQL**: `BINARY(16)` type (16-byte storage) + +**Migration Example (PostgreSQL):** +```sql +CREATE TABLE users ( + id UUID PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +``` + +**Migration Example (MySQL):** +```sql +CREATE TABLE users ( + id BINARY(16) PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + ### 3. Install dependencies ```bash @@ -576,10 +639,11 @@ package domain import ( "time" apperrors "github.com/yourname/yourproject/internal/errors" + "github.com/google/uuid" ) type Product struct { - ID int64 + ID uuid.UUID Name string Price float64 Stock int @@ -725,7 +789,7 @@ func (r *RegisterUserRequest) Validate() error { // Response DTO type UserResponse struct { - ID int64 `json:"id"` + ID uuid.UUID `json:"id"` Name string `json:"name"` Email string `json:"email"` CreatedAt time.Time `json:"created_at"` @@ -977,6 +1041,7 @@ go tool cover -html=coverage.out - [go-pwdhash](https://github.com/allisson/go-pwdhash) - Password hashing with Argon2id - [sqlutil](https://github.com/allisson/sqlutil) - SQL utilities for unified database access - [validation](https://github.com/jellydator/validation) - Advanced input validation library +- [uuid](https://github.com/google/uuid) - UUID generation including UUIDv7 support - [urfave/cli](https://github.com/urfave/cli) - CLI framework - [golang-migrate](https://github.com/golang-migrate/migrate) - Database migrations diff --git a/go.mod b/go.mod index 297c02d..2811cd4 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/allisson/sqlutil v1.10.0 github.com/go-sql-driver/mysql v1.9.3 github.com/golang-migrate/migrate/v4 v4.19.1 + github.com/google/uuid v1.6.0 github.com/jellydator/validation v1.2.0 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 diff --git a/go.sum b/go.sum index 72a3f82..bd492fc 100644 --- a/go.sum +++ b/go.sum @@ -58,6 +58,8 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +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/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= github.com/huandu/go-assert v1.1.6 h1:oaAfYxq9KNDi9qswn/6aE0EydfxSa+tWZC1KabNitYs= github.com/huandu/go-assert v1.1.6/go.mod h1:JuIfbmYG9ykwvuxoJ3V8TB5QP+3+ajIA54Y44TmkMxs= diff --git a/internal/http/http_test.go b/internal/http/http_test.go index 16cf0db..1836a6d 100644 --- a/internal/http/http_test.go +++ b/internal/http/http_test.go @@ -16,6 +16,7 @@ import ( userHttp "github.com/allisson/go-project-template/internal/user/http" "github.com/allisson/go-project-template/internal/user/http/dto" userUsecase "github.com/allisson/go-project-template/internal/user/usecase" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -42,7 +43,7 @@ func (m *MockUserUseCase) GetUserByEmail(ctx context.Context, email string) (*us return args.Get(0).(*userDomain.User), args.Error(1) } -func (m *MockUserUseCase) GetUserByID(ctx context.Context, id int64) (*userDomain.User, error) { +func (m *MockUserUseCase) GetUserByID(ctx context.Context, id uuid.UUID) (*userDomain.User, error) { args := m.Called(ctx, id) if args.Get(0) == nil { return nil, args.Error(1) @@ -246,8 +247,9 @@ func TestUserHandler_Register_Success(t *testing.T) { Password: req.Password, } + uuid1 := uuid.Must(uuid.NewV7()) expectedUser := &userDomain.User{ - ID: 1, + ID: uuid1, Name: input.Name, Email: input.Email, } @@ -266,7 +268,7 @@ func TestUserHandler_Register_Success(t *testing.T) { var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) - assert.Equal(t, float64(1), response["id"]) + assert.Equal(t, uuid1.String(), response["id"]) assert.Equal(t, input.Name, response["name"]) assert.Equal(t, input.Email, response["email"]) diff --git a/internal/outbox/domain/outbox_event.go b/internal/outbox/domain/outbox_event.go index 6d86097..57021d8 100644 --- a/internal/outbox/domain/outbox_event.go +++ b/internal/outbox/domain/outbox_event.go @@ -1,7 +1,11 @@ // Package domain defines the core outbox domain entities and types. package domain -import "time" +import ( + "time" + + "github.com/google/uuid" +) // OutboxEventStatus represents the status of an outbox event type OutboxEventStatus string @@ -14,7 +18,7 @@ const ( // OutboxEvent represents an event in the transactional outbox pattern type OutboxEvent struct { - ID int64 `db:"id" json:"id"` + ID uuid.UUID `db:"id" json:"id"` EventType string `db:"event_type" json:"event_type" fieldtag:"insert,update"` Payload string `db:"payload" json:"payload" fieldtag:"insert,update"` Status OutboxEventStatus `db:"status" json:"status" fieldtag:"insert,update"` diff --git a/internal/outbox/repository/outbox_repository_test.go b/internal/outbox/repository/outbox_repository_test.go index 684eb04..a8dae5c 100644 --- a/internal/outbox/repository/outbox_repository_test.go +++ b/internal/outbox/repository/outbox_repository_test.go @@ -7,6 +7,7 @@ import ( "github.com/DATA-DOG/go-sqlmock" "github.com/allisson/go-project-template/internal/outbox/domain" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -72,9 +73,11 @@ func TestOutboxEventRepository_GetPendingEvents(t *testing.T) { ctx := context.Background() now := time.Now() + uuid1 := uuid.Must(uuid.NewV7()) + uuid2 := uuid.Must(uuid.NewV7()) expectedEvents := []*domain.OutboxEvent{ { - ID: 1, + ID: uuid1, EventType: "user.created", Payload: `{"id": 1}`, Status: domain.OutboxEventStatusPending, @@ -83,7 +86,7 @@ func TestOutboxEventRepository_GetPendingEvents(t *testing.T) { UpdatedAt: now, }, { - ID: 2, + ID: uuid2, EventType: "user.created", Payload: `{"id": 2}`, Status: domain.OutboxEventStatusPending, @@ -141,8 +144,9 @@ func TestOutboxEventRepository_Update(t *testing.T) { ctx := context.Background() now := time.Now() + uuid1 := uuid.Must(uuid.NewV7()) event := &domain.OutboxEvent{ - ID: 1, + ID: uuid1, EventType: "user.created", Payload: `{"id": 1}`, Status: domain.OutboxEventStatusProcessed, @@ -169,8 +173,9 @@ func TestOutboxEventRepository_Update_Error(t *testing.T) { repo := NewOutboxEventRepository(db, "postgres") ctx := context.Background() + uuid1 := uuid.Must(uuid.NewV7()) event := &domain.OutboxEvent{ - ID: 999, + ID: uuid1, EventType: "user.created", Payload: `{"id": 1}`, Status: domain.OutboxEventStatusProcessed, diff --git a/internal/user/domain/user.go b/internal/user/domain/user.go index 3fa7a30..2e57944 100644 --- a/internal/user/domain/user.go +++ b/internal/user/domain/user.go @@ -5,11 +5,12 @@ import ( "time" "github.com/allisson/go-project-template/internal/errors" + "github.com/google/uuid" ) // User represents a user in the system type User struct { - ID int64 `db:"id"` + ID uuid.UUID `db:"id"` Name string `db:"name" fieldtag:"insert,update"` Email string `db:"email" fieldtag:"insert,update"` Password string `db:"password" fieldtag:"insert,update"` diff --git a/internal/user/http/dto/response.go b/internal/user/http/dto/response.go index e90e851..54d3267 100644 --- a/internal/user/http/dto/response.go +++ b/internal/user/http/dto/response.go @@ -1,13 +1,17 @@ // Package dto provides data transfer objects for the user HTTP layer. package dto -import "time" +import ( + "time" + + "github.com/google/uuid" +) // UserResponse represents the API response for a user // It excludes sensitive information like passwords and provides // a clean external representation of the user domain model type UserResponse struct { - ID int64 `json:"id"` + ID uuid.UUID `json:"id"` Name string `json:"name"` Email string `json:"email"` CreatedAt time.Time `json:"created_at"` diff --git a/internal/user/repository/user_repository.go b/internal/user/repository/user_repository.go index aee5f75..582df84 100644 --- a/internal/user/repository/user_repository.go +++ b/internal/user/repository/user_repository.go @@ -10,6 +10,7 @@ import ( "github.com/allisson/go-project-template/internal/database" "github.com/allisson/go-project-template/internal/user/domain" "github.com/allisson/sqlutil" + "github.com/google/uuid" apperrors "github.com/allisson/go-project-template/internal/errors" ) @@ -46,7 +47,7 @@ func (r *UserRepository) Create(ctx context.Context, user *domain.User) error { } // GetByID retrieves a user by ID -func (r *UserRepository) GetByID(ctx context.Context, id int64) (*domain.User, error) { +func (r *UserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) { var user domain.User opts := sqlutil.NewFindOptions(r.flavor).WithFilter("id", id) querier := database.GetTx(ctx, r.db) diff --git a/internal/user/repository/user_repository_test.go b/internal/user/repository/user_repository_test.go index 562353b..433cdd7 100644 --- a/internal/user/repository/user_repository_test.go +++ b/internal/user/repository/user_repository_test.go @@ -9,6 +9,7 @@ import ( "github.com/DATA-DOG/go-sqlmock" apperrors "github.com/allisson/go-project-template/internal/errors" "github.com/allisson/go-project-template/internal/user/domain" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -72,8 +73,9 @@ func TestUserRepository_GetByID(t *testing.T) { repo := NewUserRepository(db, "postgres") ctx := context.Background() + uuid1 := uuid.Must(uuid.NewV7()) expectedUser := &domain.User{ - ID: 1, + ID: uuid1, Name: "John Doe", Email: "john@example.com", Password: "hashed_password", @@ -85,10 +87,10 @@ func TestUserRepository_GetByID(t *testing.T) { AddRow(expectedUser.ID, expectedUser.Name, expectedUser.Email, expectedUser.Password, expectedUser.CreatedAt, expectedUser.UpdatedAt) mock.ExpectQuery("SELECT (.+) FROM users"). - WithArgs(int64(1)). + WithArgs(uuid1). WillReturnRows(rows) - user, err := repo.GetByID(ctx, 1) + user, err := repo.GetByID(ctx, uuid1) assert.NoError(t, err) assert.NotNil(t, user) assert.Equal(t, expectedUser.ID, user.ID) @@ -105,11 +107,12 @@ func TestUserRepository_GetByID_NotFound(t *testing.T) { repo := NewUserRepository(db, "postgres") ctx := context.Background() + notFoundUUID := uuid.Must(uuid.NewV7()) mock.ExpectQuery("SELECT (.+) FROM users"). - WithArgs(int64(999)). + WithArgs(notFoundUUID). WillReturnError(sql.ErrNoRows) - user, err := repo.GetByID(ctx, 999) + user, err := repo.GetByID(ctx, notFoundUUID) assert.Error(t, err) assert.Nil(t, user) assert.True(t, apperrors.Is(err, domain.ErrUserNotFound)) @@ -124,8 +127,9 @@ func TestUserRepository_GetByEmail(t *testing.T) { repo := NewUserRepository(db, "postgres") ctx := context.Background() + uuid1 := uuid.Must(uuid.NewV7()) expectedUser := &domain.User{ - ID: 1, + ID: uuid1, Name: "John Doe", Email: "john@example.com", Password: "hashed_password", diff --git a/internal/user/usecase/user_usecase.go b/internal/user/usecase/user_usecase.go index 34cbfde..f8c6e39 100644 --- a/internal/user/usecase/user_usecase.go +++ b/internal/user/usecase/user_usecase.go @@ -14,6 +14,7 @@ import ( "github.com/allisson/go-project-template/internal/user/domain" appValidation "github.com/allisson/go-project-template/internal/validation" "github.com/allisson/go-pwdhash" + "github.com/google/uuid" ) // RegisterUserInput contains the input data for user registration @@ -27,13 +28,13 @@ type RegisterUserInput struct { type UseCase interface { RegisterUser(ctx context.Context, input RegisterUserInput) (*domain.User, error) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) - GetUserByID(ctx context.Context, id int64) (*domain.User, error) + GetUserByID(ctx context.Context, id uuid.UUID) (*domain.User, error) } // UserRepository interface defines user repository operations type UserRepository interface { Create(ctx context.Context, user *domain.User) error - GetByID(ctx context.Context, id int64) (*domain.User, error) + GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) GetByEmail(ctx context.Context, email string) (*domain.User, error) } @@ -119,6 +120,7 @@ func (uc *UserUseCase) RegisterUser(ctx context.Context, input RegisterUserInput } user := &domain.User{ + ID: uuid.Must(uuid.NewV7()), Name: strings.TrimSpace(input.Name), Email: strings.TrimSpace(strings.ToLower(input.Email)), Password: hashedPassword, @@ -144,6 +146,7 @@ func (uc *UserUseCase) RegisterUser(ctx context.Context, input RegisterUserInput // Create outbox event outboxEvent := &outboxDomain.OutboxEvent{ + ID: uuid.Must(uuid.NewV7()), EventType: "user.created", Payload: string(payloadJSON), Status: outboxDomain.OutboxEventStatusPending, @@ -170,6 +173,6 @@ func (uc *UserUseCase) GetUserByEmail(ctx context.Context, email string) (*domai } // GetUserByID retrieves a user by ID -func (uc *UserUseCase) GetUserByID(ctx context.Context, id int64) (*domain.User, error) { +func (uc *UserUseCase) GetUserByID(ctx context.Context, id uuid.UUID) (*domain.User, error) { return uc.userRepo.GetByID(ctx, id) } diff --git a/internal/user/usecase/user_usecase_test.go b/internal/user/usecase/user_usecase_test.go index e2097a7..81ed39f 100644 --- a/internal/user/usecase/user_usecase_test.go +++ b/internal/user/usecase/user_usecase_test.go @@ -8,6 +8,7 @@ import ( outboxDomain "github.com/allisson/go-project-template/internal/outbox/domain" "github.com/allisson/go-project-template/internal/user/domain" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -36,12 +37,12 @@ func (m *MockUserRepository) Create(ctx context.Context, user *domain.User) erro args := m.Called(ctx, user) if args.Get(0) != nil { // Set the ID to simulate database behavior - user.ID = 1 + user.ID = uuid.Must(uuid.NewV7()) } return args.Error(0) } -func (m *MockUserRepository) GetByID(ctx context.Context, id int64) (*domain.User, error) { +func (m *MockUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) { args := m.Called(ctx, id) if args.Get(0) == nil { return nil, args.Error(1) @@ -247,8 +248,9 @@ func TestUserUseCase_GetUserByEmail_Success(t *testing.T) { require.NoError(t, err) ctx := context.Background() + uuid1 := uuid.New() expectedUser := &domain.User{ - ID: 1, + ID: uuid1, Name: "John Doe", Email: "john@example.com", } @@ -296,15 +298,16 @@ func TestUserUseCase_GetUserByID_Success(t *testing.T) { require.NoError(t, err) ctx := context.Background() + uuid1 := uuid.Must(uuid.NewV7()) expectedUser := &domain.User{ - ID: 1, + ID: uuid1, Name: "John Doe", Email: "john@example.com", } - userRepo.On("GetByID", ctx, int64(1)).Return(expectedUser, nil) + userRepo.On("GetByID", ctx, uuid1).Return(expectedUser, nil) - user, err := useCase.GetUserByID(ctx, 1) + user, err := useCase.GetUserByID(ctx, uuid1) assert.NoError(t, err) assert.NotNil(t, user) @@ -324,10 +327,11 @@ func TestUserUseCase_GetUserByID_NotFound(t *testing.T) { ctx := context.Background() notFoundError := errors.New("user not found") + notFoundUUID := uuid.Must(uuid.NewV7()) - userRepo.On("GetByID", ctx, int64(999)).Return(nil, notFoundError) + userRepo.On("GetByID", ctx, notFoundUUID).Return(nil, notFoundError) - user, err := useCase.GetUserByID(ctx, 999) + user, err := useCase.GetUserByID(ctx, notFoundUUID) assert.Error(t, err) assert.Nil(t, user) diff --git a/internal/worker/event_worker.go b/internal/worker/event_worker.go index 8f8f861..0b7dcef 100644 --- a/internal/worker/event_worker.go +++ b/internal/worker/event_worker.go @@ -99,7 +99,7 @@ func (w *EventWorker) processEvents(ctx context.Context) error { if err := w.processEvent(ctx, event); err != nil { if w.logger != nil { w.logger.Error("failed to process event", - slog.Int64("event_id", event.ID), + slog.String("event_id", event.ID.String()), slog.String("event_type", event.EventType), slog.Any("error", err), ) @@ -138,7 +138,7 @@ func (w *EventWorker) processEvents(ctx context.Context) error { func (w *EventWorker) processEvent(ctx context.Context, event *domain.OutboxEvent) error { if w.logger != nil { w.logger.Info("processing event", - slog.Int64("event_id", event.ID), + slog.String("event_id", event.ID.String()), slog.String("event_type", event.EventType), ) } diff --git a/internal/worker/event_worker_test.go b/internal/worker/event_worker_test.go index 6a367c2..8241d6f 100644 --- a/internal/worker/event_worker_test.go +++ b/internal/worker/event_worker_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/allisson/go-project-template/internal/outbox/domain" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -101,16 +102,18 @@ func TestEventWorker_ProcessEvents_Success(t *testing.T) { worker := NewEventWorker(config, txManager, outboxRepo, nil) ctx := context.Background() + uuid1 := uuid.Must(uuid.NewV7()) + uuid2 := uuid.Must(uuid.NewV7()) events := []*domain.OutboxEvent{ { - ID: 1, + ID: uuid1, EventType: "user.created", Payload: `{"user_id": 1, "name": "John Doe", "email": "john@example.com"}`, Status: domain.OutboxEventStatusPending, Retries: 0, }, { - ID: 2, + ID: uuid2, EventType: "user.created", Payload: `{"user_id": 2, "name": "Jane Doe", "email": "jane@example.com"}`, Status: domain.OutboxEventStatusPending, @@ -198,9 +201,10 @@ func TestEventWorker_ProcessEvents_InvalidJSON(t *testing.T) { worker := NewEventWorker(config, txManager, outboxRepo, nil) ctx := context.Background() + uuid1 := uuid.Must(uuid.NewV7()) events := []*domain.OutboxEvent{ { - ID: 1, + ID: uuid1, EventType: "user.created", Payload: `invalid json`, Status: domain.OutboxEventStatusPending, @@ -212,7 +216,7 @@ func TestEventWorker_ProcessEvents_InvalidJSON(t *testing.T) { txManager.On("WithTx", ctx, mock.AnythingOfType("func(context.Context) error")).Return(nil) outboxRepo.On("GetPendingEvents", ctx, config.BatchSize).Return(events, nil) outboxRepo.On("Update", ctx, mock.MatchedBy(func(e *domain.OutboxEvent) bool { - return e.ID == 1 && e.Retries == 1 && e.LastError != nil + return e.ID == uuid1 && e.Retries == 1 && e.LastError != nil })).Return(nil) err := worker.processEvents(ctx) @@ -235,9 +239,10 @@ func TestEventWorker_ProcessEvents_MaxRetriesReached(t *testing.T) { worker := NewEventWorker(config, txManager, outboxRepo, nil) ctx := context.Background() + uuid1 := uuid.Must(uuid.NewV7()) events := []*domain.OutboxEvent{ { - ID: 1, + ID: uuid1, EventType: "user.created", Payload: `invalid json`, Status: domain.OutboxEventStatusPending, @@ -249,7 +254,7 @@ func TestEventWorker_ProcessEvents_MaxRetriesReached(t *testing.T) { txManager.On("WithTx", ctx, mock.AnythingOfType("func(context.Context) error")).Return(nil) outboxRepo.On("GetPendingEvents", ctx, config.BatchSize).Return(events, nil) outboxRepo.On("Update", ctx, mock.MatchedBy(func(e *domain.OutboxEvent) bool { - return e.ID == 1 && + return e.ID == uuid1 && e.Retries == 3 && e.Status == domain.OutboxEventStatusFailed && e.LastError != nil @@ -275,9 +280,10 @@ func TestEventWorker_ProcessEvents_UpdateError(t *testing.T) { worker := NewEventWorker(config, txManager, outboxRepo, nil) ctx := context.Background() + uuid1 := uuid.Must(uuid.NewV7()) events := []*domain.OutboxEvent{ { - ID: 1, + ID: uuid1, EventType: "user.created", Payload: `{"user_id": 1}`, Status: domain.OutboxEventStatusPending, @@ -313,8 +319,9 @@ func TestEventWorker_ProcessEvent_Success(t *testing.T) { worker := NewEventWorker(config, txManager, outboxRepo, nil) ctx := context.Background() + uuid1 := uuid.Must(uuid.NewV7()) event := &domain.OutboxEvent{ - ID: 1, + ID: uuid1, EventType: "user.created", Payload: `{"user_id": 1, "name": "John Doe", "email": "john@example.com"}`, Status: domain.OutboxEventStatusPending, @@ -339,8 +346,9 @@ func TestEventWorker_ProcessEvent_UnknownEventType(t *testing.T) { worker := NewEventWorker(config, txManager, outboxRepo, nil) ctx := context.Background() + uuid1 := uuid.Must(uuid.NewV7()) event := &domain.OutboxEvent{ - ID: 1, + ID: uuid1, EventType: "unknown.event", Payload: `{"data": "test"}`, Status: domain.OutboxEventStatusPending, @@ -365,8 +373,9 @@ func TestEventWorker_ProcessEvent_InvalidJSON(t *testing.T) { worker := NewEventWorker(config, txManager, outboxRepo, nil) ctx := context.Background() + uuid1 := uuid.Must(uuid.NewV7()) event := &domain.OutboxEvent{ - ID: 1, + ID: uuid1, EventType: "user.created", Payload: `invalid json`, Status: domain.OutboxEventStatusPending, diff --git a/migrations/mysql/000001_create_users_table.up.sql b/migrations/mysql/000001_create_users_table.up.sql index e87d082..5c9ea41 100644 --- a/migrations/mysql/000001_create_users_table.up.sql +++ b/migrations/mysql/000001_create_users_table.up.sql @@ -1,6 +1,6 @@ -- Create users table CREATE TABLE IF NOT EXISTS users ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, + id BINARY(16) PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, diff --git a/migrations/mysql/000002_create_outbox_events_table.up.sql b/migrations/mysql/000002_create_outbox_events_table.up.sql index 78b9301..60eb9c7 100644 --- a/migrations/mysql/000002_create_outbox_events_table.up.sql +++ b/migrations/mysql/000002_create_outbox_events_table.up.sql @@ -1,6 +1,6 @@ -- Create outbox_events table CREATE TABLE IF NOT EXISTS outbox_events ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, + id BINARY(16) PRIMARY KEY, event_type VARCHAR(255) NOT NULL, payload TEXT NOT NULL, status VARCHAR(50) NOT NULL DEFAULT 'pending', diff --git a/migrations/postgresql/000001_create_users_table.up.sql b/migrations/postgresql/000001_create_users_table.up.sql index 4213955..1a2663b 100644 --- a/migrations/postgresql/000001_create_users_table.up.sql +++ b/migrations/postgresql/000001_create_users_table.up.sql @@ -1,6 +1,6 @@ -- Create users table CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, + id UUID PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, diff --git a/migrations/postgresql/000002_create_outbox_events_table.up.sql b/migrations/postgresql/000002_create_outbox_events_table.up.sql index ea0127b..2e761e0 100644 --- a/migrations/postgresql/000002_create_outbox_events_table.up.sql +++ b/migrations/postgresql/000002_create_outbox_events_table.up.sql @@ -1,6 +1,6 @@ -- Create outbox_events table CREATE TABLE IF NOT EXISTS outbox_events ( - id SERIAL PRIMARY KEY, + id UUID PRIMARY KEY, event_type VARCHAR(255) NOT NULL, payload TEXT NOT NULL, status VARCHAR(50) NOT NULL DEFAULT 'pending',