ultisuite-backend/internal/migration/admin_jobs.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

162 lines
4.4 KiB
Go

package migration
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/jackc/pgx/v5"
)
// AdminJob is a migration job enriched with the invited user email for admin dashboards.
type AdminJob struct {
Job
UserEmail string `json:"user_email"`
}
func (s *Service) ListProjectJobs(ctx context.Context, projectID string) ([]AdminJob, error) {
rows, err := s.db.Query(ctx, `
SELECT j.id::text, j.project_id::text, j.user_id::text, j.service, j.status,
j.cursor_json, j.stats_json, j.error, j.started_at::text, j.completed_at::text,
COALESCE(i.email, '')
FROM migration_jobs j
LEFT JOIN migration_invites i
ON i.project_id = j.project_id AND i.user_id = j.user_id
WHERE j.project_id = $1::uuid
ORDER BY COALESCE(i.email, ''), j.service ASC
`, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []AdminJob
for rows.Next() {
var row AdminJob
var cursorRaw, statsRaw []byte
if err := rows.Scan(
&row.ID, &row.ProjectID, &row.UserID, &row.Service, &row.Status,
&cursorRaw, &statsRaw, &row.Error, &row.StartedAt, &row.CompletedAt,
&row.UserEmail,
); err != nil {
return nil, err
}
_ = json.Unmarshal(cursorRaw, &row.CursorJSON)
_ = json.Unmarshal(statsRaw, &row.StatsJSON)
if row.CursorJSON == nil {
row.CursorJSON = map[string]any{}
}
if row.StatsJSON == nil {
row.StatsJSON = map[string]any{}
}
out = append(out, row)
}
return out, rows.Err()
}
func (s *Service) RetryJob(ctx context.Context, projectID, jobID string) (Job, error) {
var row Job
var cursorRaw, statsRaw []byte
err := s.db.QueryRow(ctx, `
UPDATE migration_jobs
SET status = 'pending', error = '', updated_at = NOW()
WHERE id = $1::uuid AND project_id = $2::uuid AND status = 'failed'
RETURNING id::text, project_id::text, user_id::text, service, status,
cursor_json, stats_json, error, started_at::text, completed_at::text
`, jobID, projectID).Scan(
&row.ID, &row.ProjectID, &row.UserID, &row.Service, &row.Status,
&cursorRaw, &statsRaw, &row.Error, &row.StartedAt, &row.CompletedAt,
)
if errors.Is(err, pgx.ErrNoRows) {
return Job{}, fmt.Errorf("job not found or not retryable")
}
if err != nil {
return Job{}, err
}
_ = json.Unmarshal(cursorRaw, &row.CursorJSON)
_ = json.Unmarshal(statsRaw, &row.StatsJSON)
if row.CursorJSON == nil {
row.CursorJSON = map[string]any{}
}
if row.StatsJSON == nil {
row.StatsJSON = map[string]any{}
}
return row, nil
}
func (s *Service) RetryFailedJobs(ctx context.Context, projectID string) (int64, error) {
tag, err := s.db.Exec(ctx, `
UPDATE migration_jobs
SET status = 'pending', error = '', updated_at = NOW()
WHERE project_id = $1::uuid AND status = 'failed'
`, projectID)
if err != nil {
return 0, err
}
return tag.RowsAffected(), nil
}
func (s *Service) ResetJobCursor(ctx context.Context, projectID, jobID string) (Job, error) {
tx, err := s.db.Begin(ctx)
if err != nil {
return Job{}, err
}
defer tx.Rollback(ctx)
var status string
err = tx.QueryRow(ctx, `
SELECT status FROM migration_jobs
WHERE id = $1::uuid AND project_id = $2::uuid
`, jobID, projectID).Scan(&status)
if errors.Is(err, pgx.ErrNoRows) {
return Job{}, fmt.Errorf("job not found")
}
if err != nil {
return Job{}, err
}
if status == "running" {
return Job{}, fmt.Errorf("job running; wait for completion before reset")
}
if _, err := tx.Exec(ctx, `
DELETE FROM migration_imported_items WHERE job_id = $1::uuid
`, jobID); err != nil {
return Job{}, err
}
var row Job
var cursorRaw, statsRaw []byte
err = tx.QueryRow(ctx, `
UPDATE migration_jobs
SET status = 'pending',
cursor_json = '{}'::jsonb,
stats_json = '{}'::jsonb,
error = '',
started_at = NULL,
completed_at = NULL,
updated_at = NOW()
WHERE id = $1::uuid AND project_id = $2::uuid
RETURNING id::text, project_id::text, user_id::text, service, status,
cursor_json, stats_json, error, started_at::text, completed_at::text
`, jobID, projectID).Scan(
&row.ID, &row.ProjectID, &row.UserID, &row.Service, &row.Status,
&cursorRaw, &statsRaw, &row.Error, &row.StartedAt, &row.CompletedAt,
)
if err != nil {
return Job{}, err
}
if err := tx.Commit(ctx); err != nil {
return Job{}, err
}
_ = json.Unmarshal(cursorRaw, &row.CursorJSON)
_ = json.Unmarshal(statsRaw, &row.StatsJSON)
if row.CursorJSON == nil {
row.CursorJSON = map[string]any{}
}
if row.StatsJSON == nil {
row.StatsJSON = map[string]any{}
}
return row, nil
}