Files
honeyDueAPI/internal/repositories/task_template_repo.go
T
Trey t bc3da007db
Backend CI / Test (push) Has been cancelled
Backend CI / Contract Tests (push) Has been cancelled
Backend CI / Build (push) Has been cancelled
Backend CI / Lint (push) Has been cancelled
Backend CI / Secret Scanning (push) Has been cancelled
Wire OpenTelemetry tracing — HTTP, B2, APNs, FCM, asynq, GORM (partial)
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>
2026-04-25 15:28:05 -05:00

133 lines
4.0 KiB
Go

package repositories
import (
"context"
"strings"
"gorm.io/gorm"
"github.com/treytartt/honeydue-api/internal/models"
)
// TaskTemplateRepository handles database operations for task templates
type TaskTemplateRepository struct {
db *gorm.DB
}
// NewTaskTemplateRepository creates a new task template repository
func NewTaskTemplateRepository(db *gorm.DB) *TaskTemplateRepository {
return &TaskTemplateRepository{db: db}
}
// GetAll returns all active task templates ordered by category and display order
func (r *TaskTemplateRepository) GetAll() ([]models.TaskTemplate, error) {
var templates []models.TaskTemplate
err := r.db.
Preload("Category").
Preload("Frequency").
Where("is_active = ?", true).
Order("display_order ASC, title ASC").
Find(&templates).Error
return templates, err
}
// GetByCategory returns all active templates for a specific category
func (r *TaskTemplateRepository) GetByCategory(categoryID uint) ([]models.TaskTemplate, error) {
var templates []models.TaskTemplate
err := r.db.
Preload("Category").
Preload("Frequency").
Where("is_active = ? AND category_id = ?", true, categoryID).
Order("display_order ASC, title ASC").
Find(&templates).Error
return templates, err
}
// Search searches templates by title and tags
func (r *TaskTemplateRepository) Search(query string) ([]models.TaskTemplate, error) {
var templates []models.TaskTemplate
escaped := escapeLikeWildcards(strings.ToLower(query))
searchTerm := "%" + escaped + "%"
err := r.db.
Preload("Category").
Preload("Frequency").
Where("is_active = ? AND (LOWER(title) LIKE ? OR LOWER(tags) LIKE ?)", true, searchTerm, searchTerm).
Order("display_order ASC, title ASC").
Limit(20). // Limit search results
Find(&templates).Error
return templates, err
}
// GetByID returns a single template by ID
func (r *TaskTemplateRepository) GetByID(id uint) (*models.TaskTemplate, error) {
var template models.TaskTemplate
err := r.db.
Preload("Category").
Preload("Frequency").
First(&template, id).Error
if err != nil {
return nil, err
}
return &template, nil
}
// Create creates a new task template
func (r *TaskTemplateRepository) Create(template *models.TaskTemplate) error {
return r.db.Create(template).Error
}
// Update updates an existing task template
func (r *TaskTemplateRepository) Update(template *models.TaskTemplate) error {
return r.db.Omit("Category", "Frequency").Save(template).Error
}
// Delete hard deletes a task template
func (r *TaskTemplateRepository) Delete(id uint) error {
return r.db.Delete(&models.TaskTemplate{}, id).Error
}
// GetAllIncludingInactive returns all templates including inactive ones (for admin)
func (r *TaskTemplateRepository) GetAllIncludingInactive() ([]models.TaskTemplate, error) {
var templates []models.TaskTemplate
err := r.db.
Preload("Category").
Preload("Frequency").
Order("display_order ASC, title ASC").
Find(&templates).Error
return templates, err
}
// Count returns the total count of active templates
func (r *TaskTemplateRepository) Count() (int64, error) {
var count int64
err := r.db.Model(&models.TaskTemplate{}).Where("is_active = ?", true).Count(&count).Error
return count, err
}
// GetGroupedByCategory returns templates grouped by category name
func (r *TaskTemplateRepository) GetGroupedByCategory() (map[string][]models.TaskTemplate, error) {
templates, err := r.GetAll()
if err != nil {
return nil, err
}
result := make(map[string][]models.TaskTemplate)
for _, t := range templates {
categoryName := "Uncategorized"
if t.Category != nil {
categoryName = t.Category.Name
}
result[categoryName] = append(result[categoryName], t)
}
return result, 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 *TaskTemplateRepository) WithContext(ctx context.Context) *TaskTemplateRepository {
return &TaskTemplateRepository{db: r.db.WithContext(ctx)}
}