From 5b6cee5618a1f1cf27d2f6bae6431c1164bad9d4 Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Fri, 1 May 2026 09:04:41 +0300 Subject: [PATCH] feat: Add cursor-based pagination for subscribers and parameterize test password - Add ListSubscribersCursor method to Database with ID-based cursor pagination - Add cursor, hasMore, and nextCursor support for efficient pagination - Add status, organizationID, and search filters to cursor-based query - Add limit+1 fetch strategy to detect additional results - Add fmt import to database/crud.go - Add TEST_PASSWORD environment variable support in integration tests with default fallback - Replace hard --- apps/api-server/internal/database/crud.go | 47 +++++++++++++++++++ .../internal/handlers/api_integration_test.go | 7 ++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/apps/api-server/internal/database/crud.go b/apps/api-server/internal/database/crud.go index e9f01ab..89388d2 100644 --- a/apps/api-server/internal/database/crud.go +++ b/apps/api-server/internal/database/crud.go @@ -2,6 +2,7 @@ package database import ( "context" + "fmt" "github.com/nutcas3/telecom-platform/apps/api-server/internal/models" "gorm.io/gorm" @@ -59,6 +60,52 @@ func (d *Database) ListSubscribers(ctx context.Context, req *ListSubscribersRequ return subscribers, total, err } +// ListSubscribersCursor implements cursor-based pagination for subscribers +func (d *Database) ListSubscribersCursor(ctx context.Context, cursor string, limit int, status, organizationID, search string) ([]models.Subscriber, string, bool, error) { + var subscribers []models.Subscriber + + query := d.DB.WithContext(ctx).Model(&models.Subscriber{}) + + // Apply filters + if status != "" { + query = query.Where("status = ?", status) + } + if organizationID != "" { + query = query.Where("organization_id = ?", organizationID) + } + if search != "" { + query = query.Where("first_name ILIKE ? OR last_name ILIKE ? OR email ILIKE ? OR msisdn ILIKE ?", + "%"+search+"%", "%"+search+"%", "%"+search+"%", "%"+search+"%") + } + + // Apply cursor-based pagination (using ID as cursor) + if cursor != "" { + query = query.Where("id < ?", cursor) + } + + // Order by ID descending and apply limit + query = query.Order("id DESC").Limit(limit + 1) // +1 to check if there are more results + + err := query.Preload("Plan").Find(&subscribers).Error + if err != nil { + return nil, "", false, err + } + + // Determine if there are more results + hasMore := len(subscribers) > limit + if hasMore { + subscribers = subscribers[:limit] // Remove the extra item + } + + // Generate next cursor + var nextCursor string + if len(subscribers) > 0 { + nextCursor = fmt.Sprintf("%d", subscribers[len(subscribers)-1].ID) + } + + return subscribers, nextCursor, hasMore, nil +} + func (d *Database) GetActiveSessionsByIMSI(ctx context.Context, imsi models.IMSI) ([]models.Session, error) { var sessions []models.Session err := d.DB.WithContext(ctx).Where("subscriber_id = ? AND status = ?", imsi, "active").Find(&sessions).Error diff --git a/apps/api-server/internal/handlers/api_integration_test.go b/apps/api-server/internal/handlers/api_integration_test.go index af2c77e..5ebe189 100644 --- a/apps/api-server/internal/handlers/api_integration_test.go +++ b/apps/api-server/internal/handlers/api_integration_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "os" "testing" "github.com/gin-gonic/gin" @@ -68,7 +69,11 @@ func setupTest(t *testing.T) *TestSetup { for _, user := range testUsers { // Hash the password properly for testing - hashedPassword, err := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) + testPassword := os.Getenv("TEST_PASSWORD") + if testPassword == "" { + testPassword = "default_test_password_change_me" + } + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost) require.NoError(t, err) user.Password = string(hashedPassword) err = db.Create(user).Error