ultisuite-backend/internal/migration/job_audit.go
R3D347HR4Y 7143a36c19
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
feat(mail): integrate Stalwart hosted mail and migration features
- Added configuration options for Stalwart hosted mail in .env.example.
- Updated Docker Compose to include Stalwart service with health checks.
- Introduced new API endpoints for managing mail domains and migration projects.
- Enhanced Authentik blueprints for user enrollment and post-migration security.
- Updated OAuth handling for Google and Microsoft migration processes.
- Improved error handling and response structures in the mail API.
- Added integration tests for email claiming and migration workflows.
2026-06-13 12:47:08 +02:00

159 lines
4.1 KiB
Go

package migration
import (
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/ultisuite/ulti-backend/internal/api/query"
)
const (
ItemStatusImported = "imported"
ItemStatusFailed = "failed"
ItemStatusSkipped = "skipped"
)
// JobAuditItem is one row in a migration job item audit report.
type JobAuditItem struct {
SourceID string `json:"source_id"`
RelPath string `json:"rel_path,omitempty"`
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
ImportedAt string `json:"imported_at"`
}
// JobAuditSummary counts items by status for a migration job.
type JobAuditSummary struct {
Service string `json:"service"`
Imported int64 `json:"imported"`
Failed int64 `json:"failed"`
Skipped int64 `json:"skipped"`
Total int64 `json:"total"`
ByStatus map[string]int64 `json:"by_status,omitempty"`
}
func (s *Service) verifyJobInProject(ctx context.Context, projectID, jobID string) (service string, err error) {
err = s.db.QueryRow(ctx, `
SELECT service FROM migration_jobs
WHERE id = $1::uuid AND project_id = $2::uuid
`, jobID, projectID).Scan(&service)
if errors.Is(err, pgx.ErrNoRows) {
return "", fmt.Errorf("job not found")
}
return service, err
}
func (s *Service) JobAuditSummary(ctx context.Context, projectID, jobID string) (JobAuditSummary, error) {
service, err := s.verifyJobInProject(ctx, projectID, jobID)
if err != nil {
return JobAuditSummary{}, err
}
rows, err := s.db.Query(ctx, `
SELECT status, COUNT(*)
FROM migration_imported_items
WHERE job_id = $1::uuid
GROUP BY status
`, jobID)
if err != nil {
return JobAuditSummary{}, err
}
defer rows.Close()
summary := JobAuditSummary{Service: service, ByStatus: map[string]int64{}}
for rows.Next() {
var status string
var count int64
if err := rows.Scan(&status, &count); err != nil {
return JobAuditSummary{}, err
}
summary.ByStatus[status] = count
summary.Total += count
switch status {
case ItemStatusImported:
summary.Imported = count
case ItemStatusFailed:
summary.Failed = count
case ItemStatusSkipped:
summary.Skipped = count
}
}
return summary, rows.Err()
}
func (s *Service) ListJobAudit(
ctx context.Context,
projectID, jobID, statusFilter string,
params query.ListParams,
) ([]JobAuditItem, query.PaginationMeta, error) {
if _, err := s.verifyJobInProject(ctx, projectID, jobID); err != nil {
return nil, query.PaginationMeta{}, err
}
statusFilter = normalizeAuditStatusFilter(statusFilter)
var total int64
countSQL := `SELECT COUNT(*) FROM migration_imported_items WHERE job_id = $1::uuid`
countArgs := []any{jobID}
if statusFilter != "" {
countSQL += ` AND status = $2`
countArgs = append(countArgs, statusFilter)
}
if err := s.db.QueryRow(ctx, countSQL, countArgs...).Scan(&total); err != nil {
return nil, query.PaginationMeta{}, err
}
listSQL := `
SELECT source_id, rel_path, status, reason, imported_at::text
FROM migration_imported_items
WHERE job_id = $1::uuid
`
listArgs := []any{jobID}
if statusFilter != "" {
listSQL += ` AND status = $2`
listArgs = append(listArgs, statusFilter)
}
listSQL += ` ORDER BY imported_at DESC, source_id ASC LIMIT $` + fmt.Sprint(len(listArgs)+1) +
` OFFSET $` + fmt.Sprint(len(listArgs)+2)
listArgs = append(listArgs, params.Limit(), params.Offset())
rows, err := s.db.Query(ctx, listSQL, listArgs...)
if err != nil {
return nil, query.PaginationMeta{}, err
}
defer rows.Close()
out := make([]JobAuditItem, 0, params.Limit())
for rows.Next() {
var item JobAuditItem
if err := rows.Scan(&item.SourceID, &item.RelPath, &item.Status, &item.Reason, &item.ImportedAt); err != nil {
return nil, query.PaginationMeta{}, err
}
out = append(out, item)
}
if err := rows.Err(); err != nil {
return nil, query.PaginationMeta{}, err
}
return out, params.Meta(&total), nil
}
func normalizeAuditStatusFilter(raw string) string {
switch raw {
case ItemStatusImported, ItemStatusFailed, ItemStatusSkipped:
return raw
default:
return ""
}
}
func incJobStat(stats map[string]any, key string) {
if stats == nil {
return
}
v, _ := stats[key].(float64)
stats[key] = v + 1
}