diff --git a/internal/repositories/postgres/lifecycle_repo.go b/internal/repositories/postgres/lifecycle_repo.go index fdf38d593..0c0daa929 100644 --- a/internal/repositories/postgres/lifecycle_repo.go +++ b/internal/repositories/postgres/lifecycle_repo.go @@ -23,11 +23,12 @@ func NewLifecycleRepository(db DB) *LifecycleRepository { } func (r *LifecycleRepository) Create(ctx context.Context, rule *domain.LifecycleRule) error { + tenantID := appcontext.TenantIDFromContext(ctx) query := ` - INSERT INTO lifecycle_rules (id, user_id, bucket_name, prefix, expiration_days, enabled, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + INSERT INTO lifecycle_rules (id, user_id, tenant_id, bucket_name, prefix, expiration_days, enabled, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ` - _, err := r.db.Exec(ctx, query, rule.ID, rule.UserID, rule.BucketName, rule.Prefix, rule.ExpirationDays, rule.Enabled, rule.CreatedAt, rule.UpdatedAt) + _, err := r.db.Exec(ctx, query, rule.ID, rule.UserID, tenantID, rule.BucketName, rule.Prefix, rule.ExpirationDays, rule.Enabled, rule.CreatedAt, rule.UpdatedAt) if err != nil { return errors.Wrap(errors.Internal, "failed to create lifecycle rule", err) } @@ -35,15 +36,16 @@ func (r *LifecycleRepository) Create(ctx context.Context, rule *domain.Lifecycle } func (r *LifecycleRepository) Get(ctx context.Context, id uuid.UUID) (*domain.LifecycleRule, error) { - userId := appcontext.UserIDFromContext(ctx) + userID := appcontext.UserIDFromContext(ctx) + tenantID := appcontext.TenantIDFromContext(ctx) query := ` - SELECT id, user_id, bucket_name, prefix, expiration_days, enabled, created_at, updated_at + SELECT id, user_id, tenant_id, bucket_name, prefix, expiration_days, enabled, created_at, updated_at FROM lifecycle_rules - WHERE id = $1 AND user_id = $2 + WHERE id = $1 AND user_id = $2 AND tenant_id = $3 ` var rule domain.LifecycleRule - err := r.db.QueryRow(ctx, query, id, userId).Scan( - &rule.ID, &rule.UserID, &rule.BucketName, &rule.Prefix, &rule.ExpirationDays, &rule.Enabled, &rule.CreatedAt, &rule.UpdatedAt, + err := r.db.QueryRow(ctx, query, id, userID, tenantID).Scan( + &rule.ID, &rule.UserID, &rule.TenantID, &rule.BucketName, &rule.Prefix, &rule.ExpirationDays, &rule.Enabled, &rule.CreatedAt, &rule.UpdatedAt, ) if err != nil { if stdlib_errors.Is(err, pgx.ErrNoRows) { @@ -55,23 +57,24 @@ func (r *LifecycleRepository) Get(ctx context.Context, id uuid.UUID) (*domain.Li } func (r *LifecycleRepository) List(ctx context.Context, bucketName string) ([]*domain.LifecycleRule, error) { - userId := appcontext.UserIDFromContext(ctx) + userID := appcontext.UserIDFromContext(ctx) + tenantID := appcontext.TenantIDFromContext(ctx) query := ` - SELECT id, user_id, bucket_name, prefix, expiration_days, enabled, created_at, updated_at + SELECT id, user_id, tenant_id, bucket_name, prefix, expiration_days, enabled, created_at, updated_at FROM lifecycle_rules - WHERE bucket_name = $1 AND user_id = $2 + WHERE bucket_name = $1 AND user_id = $2 AND tenant_id = $3 ORDER BY created_at DESC ` - rows, err := r.db.Query(ctx, query, bucketName, userId) + rows, err := r.db.Query(ctx, query, bucketName, userID, tenantID) if err != nil { return nil, errors.Wrap(errors.Internal, "failed to list lifecycle rules", err) } defer rows.Close() - var rules []*domain.LifecycleRule + rules := make([]*domain.LifecycleRule, 0) for rows.Next() { var rule domain.LifecycleRule - if err := rows.Scan(&rule.ID, &rule.UserID, &rule.BucketName, &rule.Prefix, &rule.ExpirationDays, &rule.Enabled, &rule.CreatedAt, &rule.UpdatedAt); err != nil { + if err := rows.Scan(&rule.ID, &rule.UserID, &rule.TenantID, &rule.BucketName, &rule.Prefix, &rule.ExpirationDays, &rule.Enabled, &rule.CreatedAt, &rule.UpdatedAt); err != nil { return nil, errors.Wrap(errors.Internal, "failed to scan lifecycle rule", err) } rules = append(rules, &rule) @@ -80,9 +83,10 @@ func (r *LifecycleRepository) List(ctx context.Context, bucketName string) ([]*d } func (r *LifecycleRepository) Delete(ctx context.Context, id uuid.UUID) error { - userId := appcontext.UserIDFromContext(ctx) - query := `DELETE FROM lifecycle_rules WHERE id = $1 AND user_id = $2` - cmd, err := r.db.Exec(ctx, query, id, userId) + userID := appcontext.UserIDFromContext(ctx) + tenantID := appcontext.TenantIDFromContext(ctx) + query := `DELETE FROM lifecycle_rules WHERE id = $1 AND user_id = $2 AND tenant_id = $3` + cmd, err := r.db.Exec(ctx, query, id, userID, tenantID) if err != nil { return errors.Wrap(errors.Internal, "failed to delete lifecycle rule", err) } @@ -96,7 +100,7 @@ func (r *LifecycleRepository) Delete(ctx context.Context, id uuid.UUID) error { // This is intended for background workers and does not filter by user. func (r *LifecycleRepository) GetEnabledRules(ctx context.Context) ([]*domain.LifecycleRule, error) { query := ` - SELECT id, user_id, bucket_name, prefix, expiration_days, enabled, created_at, updated_at + SELECT id, user_id, tenant_id, bucket_name, prefix, expiration_days, enabled, created_at, updated_at FROM lifecycle_rules WHERE enabled = TRUE ` @@ -106,10 +110,10 @@ func (r *LifecycleRepository) GetEnabledRules(ctx context.Context) ([]*domain.Li } defer rows.Close() - var rules []*domain.LifecycleRule + rules := make([]*domain.LifecycleRule, 0) for rows.Next() { var rule domain.LifecycleRule - if err := rows.Scan(&rule.ID, &rule.UserID, &rule.BucketName, &rule.Prefix, &rule.ExpirationDays, &rule.Enabled, &rule.CreatedAt, &rule.UpdatedAt); err != nil { + if err := rows.Scan(&rule.ID, &rule.UserID, &rule.TenantID, &rule.BucketName, &rule.Prefix, &rule.ExpirationDays, &rule.Enabled, &rule.CreatedAt, &rule.UpdatedAt); err != nil { return nil, errors.Wrap(errors.Internal, "failed to scan lifecycle rule", err) } rules = append(rules, &rule) diff --git a/internal/repositories/postgres/lifecycle_repo_unit_test.go b/internal/repositories/postgres/lifecycle_repo_unit_test.go index 3ff405de7..47e748635 100644 --- a/internal/repositories/postgres/lifecycle_repo_unit_test.go +++ b/internal/repositories/postgres/lifecycle_repo_unit_test.go @@ -22,7 +22,9 @@ func TestLifecycleRepository(t *testing.T) { t.Parallel() ctx := context.Background() userID := uuid.New() + tenantID := uuid.New() ctx = appcontext.WithUserID(ctx, userID) + ctx = appcontext.WithTenantID(ctx, tenantID) bucketName := "test-bucket" t.Run("Create", func(t *testing.T) { @@ -42,7 +44,7 @@ func TestLifecycleRepository(t *testing.T) { } mock.ExpectExec("INSERT INTO lifecycle_rules"). - WithArgs(rule.ID, rule.UserID, rule.BucketName, rule.Prefix, rule.ExpirationDays, rule.Enabled, rule.CreatedAt, rule.UpdatedAt). + WithArgs(rule.ID, rule.UserID, tenantID, rule.BucketName, rule.Prefix, rule.ExpirationDays, rule.Enabled, rule.CreatedAt, rule.UpdatedAt). WillReturnResult(pgxmock.NewResult("INSERT", 1)) err := repo.Create(ctx, rule) @@ -57,9 +59,9 @@ func TestLifecycleRepository(t *testing.T) { id := uuid.New() mock.ExpectQuery(selectLifecycleRules). - WithArgs(id, userID). - WillReturnRows(pgxmock.NewRows([]string{"id", "user_id", "bucket_name", "prefix", "expiration_days", "enabled", "created_at", "updated_at"}). - AddRow(id, userID, bucketName, testLifecyclePrefix, 30, true, time.Now(), time.Now())) + WithArgs(id, userID, tenantID). + WillReturnRows(pgxmock.NewRows([]string{"id", "user_id", "tenant_id", "bucket_name", "prefix", "expiration_days", "enabled", "created_at", "updated_at"}). + AddRow(id, userID, tenantID, bucketName, testLifecyclePrefix, 30, true, time.Now(), time.Now())) rule, err := repo.Get(ctx, id) require.NoError(t, err) @@ -74,7 +76,7 @@ func TestLifecycleRepository(t *testing.T) { id := uuid.New() mock.ExpectExec("DELETE FROM lifecycle_rules"). - WithArgs(id, userID). + WithArgs(id, userID, tenantID). WillReturnResult(pgxmock.NewResult("DELETE", 1)) err := repo.Delete(ctx, id) diff --git a/internal/repositories/postgres/migrations/109_lifecycle_rules_tenant_id.down.sql b/internal/repositories/postgres/migrations/109_lifecycle_rules_tenant_id.down.sql new file mode 100644 index 000000000..412888976 --- /dev/null +++ b/internal/repositories/postgres/migrations/109_lifecycle_rules_tenant_id.down.sql @@ -0,0 +1,2 @@ +-- +goose Down +ALTER TABLE lifecycle_rules DROP COLUMN IF EXISTS tenant_id; diff --git a/internal/repositories/postgres/migrations/109_lifecycle_rules_tenant_id.up.sql b/internal/repositories/postgres/migrations/109_lifecycle_rules_tenant_id.up.sql new file mode 100644 index 000000000..0f0fbd906 --- /dev/null +++ b/internal/repositories/postgres/migrations/109_lifecycle_rules_tenant_id.up.sql @@ -0,0 +1,6 @@ +-- +goose Up +ALTER TABLE lifecycle_rules ADD COLUMN IF NOT EXISTS tenant_id UUID REFERENCES tenants(id) ON DELETE SET NULL; +CREATE INDEX IF NOT EXISTS idx_lifecycle_rules_tenant ON lifecycle_rules(tenant_id); + +-- Backfill existing data using default_tenant_id from users table +UPDATE lifecycle_rules lr SET tenant_id = u.default_tenant_id FROM users u WHERE lr.user_id = u.id AND lr.tenant_id IS NULL;