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>
215 lines
6.4 KiB
Go
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
|
|
}
|