// Package messagesvc // Path: /internal/platform/services/messages // File: service.go package messagesvc import ( "context" "database/sql" "errors" "time" domain "synlotto-website/internal/domain/messages" ) // Service implements domain.Service. type Service struct { DB *sql.DB Dialect string // "postgres", "mysql", "sqlite" Now func() time.Time Timeout time.Duration } func New(db *sql.DB, opts ...func(*Service)) *Service { s := &Service{ DB: db, Dialect: "mysql", // default; works with LastInsertId Now: time.Now, Timeout: 5 * time.Second, } for _, opt := range opts { opt(s) } return s } // Ensure *Service satisfies the domain interface. var _ domain.MessageService = (*Service)(nil) // ToDo: Needs a userId on table or rename the recipiant id.. but then again dont want to expose userids to users for sending. func (s *Service) ListInbox(userID int64) ([]domain.Message, error) { ctx, cancel := context.WithTimeout(context.Background(), s.Timeout) defer cancel() q := ` SELECT id, senderId, recipientId, subject, message, is_read, is_archived, created_at FROM users_messages WHERE recipientId = ? AND is_archived = FALSE ORDER BY created_at DESC` q = s.bind(q) rows, err := s.DB.QueryContext(ctx, q, userID) if err != nil { return nil, err } defer rows.Close() var out []domain.Message for rows.Next() { var m domain.Message if err := rows.Scan(&m.ID, &m.SenderId, &m.RecipientId, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt); err != nil { return nil, err } out = append(out, m) } return out, rows.Err() } func (s *Service) ListArchived(userID int64) ([]domain.Message, error) { ctx, cancel := context.WithTimeout(context.Background(), s.Timeout) defer cancel() q := ` SELECT id, senderId, recipientId, subject, message, is_read, is_archived, created_at FROM users_messages WHERE recipientId = ? AND is_archived = TRUE ORDER BY created_at DESC` q = s.bind(q) rows, err := s.DB.QueryContext(ctx, q, userID) if err != nil { return nil, err } defer rows.Close() var out []domain.Message for rows.Next() { var m domain.Message if err := rows.Scan(&m.ID, &m.SenderId, &m.RecipientId, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt); err != nil { return nil, err } out = append(out, m) } return out, rows.Err() } func (s *Service) GetByID(userID, id int64) (*domain.Message, error) { ctx, cancel := context.WithTimeout(context.Background(), s.Timeout) defer cancel() q := ` SELECT id, senderId, recipientId, subject, message, is_read, is_archived, created_at FROM users_messages WHERE recipientId = ? AND id = ?` q = s.bind(q) var m domain.Message err := s.DB.QueryRowContext(ctx, q, userID, id). Scan(&m.ID, &m.SenderId, &m.RecipientId, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, err } return &m, nil } func (s *Service) Create(userID int64, in domain.CreateMessageInput) (int64, error) { ctx, cancel := context.WithTimeout(context.Background(), s.Timeout) defer cancel() switch s.Dialect { case "postgres": const q = ` INSERT INTO messages (id, senderId, recipientId, subject, message, is_read, is_archived, created_at) VALUES ($1, $2, $3, $4, $5, FALSE, FALSE, NOW()) RETURNING id` var id int64 if err := s.DB.QueryRowContext(ctx, q, userID, "", in.RecipientID, in.Subject, in.Body).Scan(&id); err != nil { return 0, err } return id, nil default: // mysql/sqlite const q = ` INSERT INTO messages (id, senderId, recipientId, subject, message is_read, is_archived, created_at) VALUES (?, ?, ?, ?, ?, FALSE, FALSE, CURRENT_TIMESTAMP)` res, err := s.DB.ExecContext(ctx, q, userID, "", in.RecipientID, in.Subject, in.Body) if err != nil { return 0, err } return res.LastInsertId() } } // bind replaces '?' with '$1..' only for Postgres. For MySQL/SQLite it returns q unchanged. func (s *Service) bind(q string) string { if s.Dialect != "postgres" { return q } n := 0 out := make([]byte, 0, len(q)+8) for i := 0; i < len(q); i++ { if q[i] == '?' { n++ out = append(out, '$') out = append(out, []byte(intToStr(n))...) continue } out = append(out, q[i]) } return string(out) } // ToDo: helper dont think it should be here. func intToStr(n int) string { if n == 0 { return "0" } var b [12]byte i := len(b) for n > 0 { i-- b[i] = byte('0' + n%10) n /= 10 } return string(b[i:]) }