Yimaru-BackEnd/internal/repository/profile_field_options.go
Yared Yemane a5acd00637 Add admin-managed field options API for configurable dropdowns.
Store options in field_options with public /field-options and admin CRUD; validate learner profile values on update.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 09:21:36 -07:00

215 lines
6.4 KiB
Go

package repository
import (
"Yimaru-Backend/internal/domain"
"Yimaru-Backend/internal/ports"
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
func NewProfileFieldOptionStore(s *Store) ports.ProfileFieldOptionStore { return s }
func profileFieldOptionToDomain(
id int64,
fieldKey, code, label string,
displayOrder int32,
status string,
createdAt pgtype.Timestamptz,
updatedAt pgtype.Timestamptz,
) domain.ProfileFieldOption {
return domain.ProfileFieldOption{
ID: id,
FieldKey: fieldKey,
Code: code,
Label: label,
DisplayOrder: displayOrder,
Status: status,
CreatedAt: createdAt.Time,
UpdatedAt: timePtr(updatedAt),
}
}
func (s *Store) CreateProfileFieldOption(ctx context.Context, input domain.CreateProfileFieldOptionInput) (domain.ProfileFieldOption, error) {
displayOrder := int32(0)
if input.DisplayOrder != nil {
displayOrder = *input.DisplayOrder
}
status := domain.ProfileFieldOptionStatusActive
if input.Status != nil {
status = *input.Status
}
row := s.conn.QueryRow(ctx, `
INSERT INTO field_options (field_key, code, label, display_order, status)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, field_key, code, label, display_order, status, created_at, updated_at
`, input.FieldKey, input.Code, input.Label, displayOrder, status)
var (
id int64
fieldKey string
code string
label string
orderVal int32
optionStatus string
createdAt pgtype.Timestamptz
updatedAt pgtype.Timestamptz
)
if err := row.Scan(&id, &fieldKey, &code, &label, &orderVal, &optionStatus, &createdAt, &updatedAt); err != nil {
return domain.ProfileFieldOption{}, err
}
return profileFieldOptionToDomain(id, fieldKey, code, label, orderVal, optionStatus, createdAt, updatedAt), nil
}
func (s *Store) UpdateProfileFieldOption(ctx context.Context, id int64, input domain.UpdateProfileFieldOptionInput) (domain.ProfileFieldOption, error) {
row := s.conn.QueryRow(ctx, `
UPDATE field_options
SET label = COALESCE($2, label),
display_order = COALESCE($3, display_order),
status = COALESCE($4, status),
updated_at = NOW()
WHERE id = $1
RETURNING id, field_key, code, label, display_order, status, created_at, updated_at
`, id, input.Label, input.DisplayOrder, input.Status)
var (
optionID int64
fieldKey string
code string
label string
orderVal int32
optionStatus string
createdAt pgtype.Timestamptz
updatedAt pgtype.Timestamptz
)
if err := row.Scan(&optionID, &fieldKey, &code, &label, &orderVal, &optionStatus, &createdAt, &updatedAt); err != nil {
return domain.ProfileFieldOption{}, err
}
return profileFieldOptionToDomain(optionID, fieldKey, code, label, orderVal, optionStatus, createdAt, updatedAt), nil
}
func (s *Store) GetProfileFieldOptionByID(ctx context.Context, id int64, includeInactive bool) (domain.ProfileFieldOption, error) {
row := s.conn.QueryRow(ctx, `
SELECT id, field_key, code, label, display_order, status, created_at, updated_at
FROM field_options
WHERE id = $1
AND ($2::boolean = TRUE OR status = 'ACTIVE')
`, id, includeInactive)
var (
optionID int64
fieldKey string
code string
label string
orderVal int32
optionStatus string
createdAt pgtype.Timestamptz
updatedAt pgtype.Timestamptz
)
if err := row.Scan(&optionID, &fieldKey, &code, &label, &orderVal, &optionStatus, &createdAt, &updatedAt); err != nil {
return domain.ProfileFieldOption{}, err
}
return profileFieldOptionToDomain(optionID, fieldKey, code, label, orderVal, optionStatus, createdAt, updatedAt), nil
}
func (s *Store) ListProfileFieldOptions(ctx context.Context, fieldKey *string, status *string, limit, offset int32) ([]domain.ProfileFieldOption, int64, error) {
rows, err := s.conn.Query(ctx, `
SELECT id, field_key, code, label, display_order, status, created_at, updated_at
FROM field_options
WHERE ($1::text IS NULL OR field_key = $1)
AND ($2::text IS NULL OR status = $2)
ORDER BY field_key ASC, display_order ASC, id ASC
LIMIT $3 OFFSET $4
`, toPgText(fieldKey), toPgText(status), limit, offset)
if err != nil {
return nil, 0, err
}
defer rows.Close()
options := make([]domain.ProfileFieldOption, 0)
for rows.Next() {
var (
optionID int64
fk string
code string
label string
orderVal int32
optionStatus string
createdAt pgtype.Timestamptz
updatedAt pgtype.Timestamptz
)
if err := rows.Scan(&optionID, &fk, &code, &label, &orderVal, &optionStatus, &createdAt, &updatedAt); err != nil {
return nil, 0, err
}
options = append(options, profileFieldOptionToDomain(optionID, fk, code, label, orderVal, optionStatus, createdAt, updatedAt))
}
if err := rows.Err(); err != nil {
return nil, 0, err
}
var totalCount int64
if err := s.conn.QueryRow(ctx, `
SELECT COUNT(*)
FROM field_options
WHERE ($1::text IS NULL OR field_key = $1)
AND ($2::text IS NULL OR status = $2)
`, toPgText(fieldKey), toPgText(status)).Scan(&totalCount); err != nil {
return nil, 0, err
}
return options, totalCount, nil
}
func (s *Store) ListActiveProfileFieldOptions(ctx context.Context, fieldKey *string) ([]domain.ProfileFieldOption, error) {
active := domain.ProfileFieldOptionStatusActive
options, _, err := s.ListProfileFieldOptions(ctx, fieldKey, &active, 500, 0)
return options, err
}
func (s *Store) IsActiveProfileFieldOption(ctx context.Context, fieldKey, code string) (bool, error) {
var exists bool
err := s.conn.QueryRow(ctx, `
SELECT EXISTS (
SELECT 1 FROM field_options
WHERE field_key = $1 AND code = $2 AND status = 'ACTIVE'
)
`, fieldKey, code).Scan(&exists)
return exists, err
}
func (s *Store) ListDistinctFieldKeys(ctx context.Context, activeOnly bool) ([]string, error) {
rows, err := s.conn.Query(ctx, `
SELECT DISTINCT field_key
FROM field_options
WHERE ($1::boolean = FALSE OR status = 'ACTIVE')
ORDER BY field_key ASC
`, activeOnly)
if err != nil {
return nil, err
}
defer rows.Close()
keys := make([]string, 0)
for rows.Next() {
var key string
if err := rows.Scan(&key); err != nil {
return nil, err
}
keys = append(keys, key)
}
return keys, rows.Err()
}
func (s *Store) DeleteProfileFieldOption(ctx context.Context, id int64) error {
cmd, err := s.conn.Exec(ctx, `DELETE FROM field_options WHERE id = $1`, id)
if err != nil {
return err
}
if cmd.RowsAffected() == 0 {
return pgx.ErrNoRows
}
return nil
}