fix(repo,service): SCALE-002 — push pagination into SQL for target/issuer/team/agent_group

Sprint 2 unified-master-audit closure. Pre-fix four service List
endpoints (target, issuer, team, agent_group) called repoFoo.List(ctx)
to fetch the full table then sliced in memory:

    rows, _ := s.repo.List(ctx)
    total := int64(len(rows))
    start := (page - 1) * perPage
    end := start + perPage
    return rows[start:end], total, nil

This page-sliced in memory pattern marshals every row per request —
fine on small fleets but unacceptable for multi-tenant or large-fleet
deploys. The agent_group case was worse — the service explicitly
ignored page/perPage and returned the entire slice.

Fix:
  - New ListPaginated(ctx, limit, offset) method on each of the four
    repositories. Postgres implementations push LIMIT + OFFSET into
    the SQL plus a SELECT COUNT(*) for the total. Mirrors the cursor
    pattern already in internal/repository/postgres/certificate.go.
  - Each ListPaginated normalises limit≤0→50 and offset<0→0,
    matching the service-layer defaults that already existed.
  - Repository interfaces grow the new method so adapters stay
    swappable.
  - Service List methods now call repoFoo.ListPaginated(ctx, perPage,
    (page-1)*perPage) directly — no more memory-slice.
  - AgentGroupService.ListAgentGroups closes the Bundle E / Audit
    L-020 'page/perPage unused' gap.

Test changes:
  - sliceWindow generic helper in testutil_test.go mirrors the SQL
    LIMIT/OFFSET semantics for in-memory mocks.
  - Six mock implementers (lifecycle_test, testutil_test x2,
    agent_group_test, team_test) gain ListPaginated methods.
  - TestTeamService_List_SCALE002_PaginationPropagatesToRepo pins
    the page=2, perPage=3 → 3 rows of 10 invariant.

Closes SCALE-002.
This commit is contained in:
shankar0123
2026-05-16 04:01:45 +00:00
parent 8f2e5771db
commit a485e31f63
13 changed files with 335 additions and 56 deletions
+7 -15
View File
@@ -89,6 +89,11 @@ func NewTargetService(
}
// List returns a paginated list of deployment targets.
//
// SCALE-002 closure (Sprint 2, 2026-05-16): pagination is pushed into
// the SQL layer via TargetRepository.ListPaginated. Pre-fix this called
// targetRepo.List(ctx) and sliced in memory, which marshalled the
// entire targets table per request — a problem on large-fleet deploys.
func (s *TargetService) List(ctx context.Context, page, perPage int) ([]*domain.DeploymentTarget, int64, error) {
if page < 1 {
page = 1
@@ -96,21 +101,8 @@ func (s *TargetService) List(ctx context.Context, page, perPage int) ([]*domain.
if perPage < 1 {
perPage = 50
}
targets, err := s.targetRepo.List(ctx)
if err != nil {
return nil, 0, fmt.Errorf("failed to list targets: %w", err)
}
total := int64(len(targets))
start := (page - 1) * perPage
if start >= int(total) {
return nil, total, nil
}
end := start + perPage
if end > int(total) {
end = int(total)
}
return targets[start:end], total, nil
offset := (page - 1) * perPage
return s.targetRepo.ListPaginated(ctx, perPage, offset)
}
// Get retrieves a deployment target by ID.