bc3da007db
Step 1 — OTel SDK: cmd/api and cmd/worker initialize a tracer provider that exports OTLP/HTTP to obs.88oakapps.com (Jaeger all-in-one). Sampling is AlwaysSample in dev (DEBUG=true) and TraceIDRatioBased(0.1) in prod, overridable via OTEL_TRACES_SAMPLER_ARG. Service names are honeydue-api and honeydue-worker. otelecho.Middleware opens a span per HTTP request. Step 2 — Manual spans: storage_service.Upload now takes ctx and emits storage.upload + b2.PutObject spans (size_bytes, key, mime_type, bucket, result attrs). APNs Send/SendWithCategory and FCM sendOne emit per-token spans with topic, status_code, reason. Asynq middleware emits asynq.handle:<task_type> per job with retry/payload attrs and records asynq_job_duration_seconds. Step 3 — Database: otelgorm plugin registered in database.Connect, so any SQL emitted via db.WithContext(ctx) attaches to the request span. Every repository now exposes WithContext(ctx) *XRepository as the migration helper. TaskService.ListTasks and GetTasksByResidence are migrated end-to-end (ctx threaded through handler → service → repo); remaining services adopt the same pattern incrementally — pre-migration methods still emit untraced SQL via the unchanged db field. OBS_TRACES_URL and OBS_INGEST_TOKEN flow from deploy/prod.env → honeydue-secrets → api+worker Deployments via secretKeyRef (optional). 02-setup-secrets.sh sources them from prod.env on next run; manifests mark both env vars optional so the deployment rolls without traces if the secret is absent. ch15 observability doc now lists what produces spans today vs the remaining migration work, with the explicit per-method pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
116 lines
3.2 KiB
Go
116 lines
3.2 KiB
Go
package repositories
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/treytartt/honeydue-api/internal/models"
|
|
)
|
|
|
|
var (
|
|
ErrAdminNotFound = errors.New("admin user not found")
|
|
ErrAdminExists = errors.New("admin user already exists")
|
|
)
|
|
|
|
// AdminRepository handles admin user database operations
|
|
type AdminRepository struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewAdminRepository creates a new admin repository
|
|
func NewAdminRepository(db *gorm.DB) *AdminRepository {
|
|
return &AdminRepository{db: db}
|
|
}
|
|
|
|
// FindByID finds an admin user by ID
|
|
func (r *AdminRepository) FindByID(id uint) (*models.AdminUser, error) {
|
|
var admin models.AdminUser
|
|
if err := r.db.First(&admin, id).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, ErrAdminNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return &admin, nil
|
|
}
|
|
|
|
// FindByEmail finds an admin user by email (case-insensitive)
|
|
func (r *AdminRepository) FindByEmail(email string) (*models.AdminUser, error) {
|
|
var admin models.AdminUser
|
|
if err := r.db.Where("LOWER(email) = LOWER(?)", email).First(&admin).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, ErrAdminNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return &admin, nil
|
|
}
|
|
|
|
// Create creates a new admin user
|
|
func (r *AdminRepository) Create(admin *models.AdminUser) error {
|
|
// Check if email already exists
|
|
var count int64
|
|
if err := r.db.Model(&models.AdminUser{}).Where("LOWER(email) = LOWER(?)", admin.Email).Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
if count > 0 {
|
|
return ErrAdminExists
|
|
}
|
|
|
|
return r.db.Create(admin).Error
|
|
}
|
|
|
|
// Update updates an admin user
|
|
func (r *AdminRepository) Update(admin *models.AdminUser) error {
|
|
return r.db.Save(admin).Error
|
|
}
|
|
|
|
// Delete deletes an admin user
|
|
func (r *AdminRepository) Delete(id uint) error {
|
|
return r.db.Delete(&models.AdminUser{}, id).Error
|
|
}
|
|
|
|
// UpdateLastLogin updates the last login timestamp
|
|
func (r *AdminRepository) UpdateLastLogin(id uint) error {
|
|
now := time.Now()
|
|
return r.db.Model(&models.AdminUser{}).Where("id = ?", id).Update("last_login", now).Error
|
|
}
|
|
|
|
// List returns all admin users with pagination
|
|
func (r *AdminRepository) List(page, pageSize int) ([]models.AdminUser, int64, error) {
|
|
var admins []models.AdminUser
|
|
var total int64
|
|
|
|
// Get total count
|
|
if err := r.db.Model(&models.AdminUser{}).Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Get paginated results
|
|
offset := (page - 1) * pageSize
|
|
if err := r.db.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&admins).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return admins, total, nil
|
|
}
|
|
|
|
// ExistsByEmail checks if an admin user with the given email exists
|
|
func (r *AdminRepository) ExistsByEmail(email string) (bool, error) {
|
|
var count int64
|
|
if err := r.db.Model(&models.AdminUser{}).Where("LOWER(email) = LOWER(?)", email).Count(&count).Error; err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
// WithContext returns a copy of the repository whose underlying *gorm.DB carries
|
|
// the supplied context. SQL emitted via this copy gets attached to ctx's trace span
|
|
// (when otelgorm is registered) and respects ctx cancellation/deadlines.
|
|
func (r *AdminRepository) WithContext(ctx context.Context) *AdminRepository {
|
|
return &AdminRepository{db: r.db.WithContext(ctx)}
|
|
}
|