Continued work on messages and notifications.
This commit is contained in:
@@ -58,8 +58,12 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
domainMsgs "synlotto-website/internal/domain/messages"
|
||||
domainNotifs "synlotto-website/internal/domain/notifications"
|
||||
weberr "synlotto-website/internal/http/error"
|
||||
databasePlatform "synlotto-website/internal/platform/database"
|
||||
messagesvc "synlotto-website/internal/platform/services/messages"
|
||||
notifysvc "synlotto-website/internal/platform/services/notifications"
|
||||
|
||||
"synlotto-website/internal/platform/config"
|
||||
"synlotto-website/internal/platform/csrf"
|
||||
@@ -78,6 +82,11 @@ type App struct {
|
||||
Router *gin.Engine
|
||||
Handler http.Handler
|
||||
Server *http.Server
|
||||
|
||||
Services struct {
|
||||
Messages domainMsgs.MessageService
|
||||
Notifications domainNotifs.NotificationService
|
||||
}
|
||||
}
|
||||
|
||||
func Load(configPath string) (*App, error) {
|
||||
@@ -119,6 +128,9 @@ func Load(configPath string) (*App, error) {
|
||||
Router: router,
|
||||
}
|
||||
|
||||
app.Services.Messages = messagesvc.New(db)
|
||||
app.Services.Notifications = notifysvc.New(db)
|
||||
|
||||
// Inject *App into Gin context for handler access
|
||||
router.Use(func(c *gin.Context) {
|
||||
c.Set("app", app)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// ToDo: not currently used and need to carve out sql
|
||||
// Package messagesvc
|
||||
// Path: /internal/platform/services/messages
|
||||
// File: service.go
|
||||
|
||||
package messagesvc
|
||||
|
||||
import (
|
||||
@@ -7,13 +10,13 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
accountMessageHandler "synlotto-website/internal/handlers/account/messages"
|
||||
domain "synlotto-website/internal/domain/messages"
|
||||
)
|
||||
|
||||
// Service implements accountMessageHandler.MessageService.
|
||||
// Service implements domain.Service.
|
||||
type Service struct {
|
||||
DB *sql.DB
|
||||
Dialect string // "postgres", "mysql", "sqlite" (affects INSERT id retrieval)
|
||||
Dialect string // "postgres", "mysql", "sqlite"
|
||||
Now func() time.Time
|
||||
Timeout time.Duration
|
||||
}
|
||||
@@ -21,7 +24,7 @@ type Service struct {
|
||||
func New(db *sql.DB, opts ...func(*Service)) *Service {
|
||||
s := &Service{
|
||||
DB: db,
|
||||
Dialect: "mysql", // sane default for LastInsertId (works for mysql/sqlite)
|
||||
Dialect: "mysql", // default; works with LastInsertId
|
||||
Now: time.Now,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
@@ -31,56 +34,58 @@ func New(db *sql.DB, opts ...func(*Service)) *Service {
|
||||
return s
|
||||
}
|
||||
|
||||
// WithDialect sets SQL dialect hints: "postgres" uses RETURNING id.
|
||||
func WithDialect(d string) func(*Service) { return func(s *Service) { s.Dialect = d } }
|
||||
// Ensure *Service satisfies the domain interface.
|
||||
var _ domain.MessageService = (*Service)(nil)
|
||||
|
||||
// WithTimeout overrides per-call context timeout.
|
||||
func WithTimeout(d time.Duration) func(*Service) { return func(s *Service) { s.Timeout = d } }
|
||||
|
||||
func (s *Service) ListInbox(userID int64) ([]accountMessageHandler.Message, error) {
|
||||
func (s *Service) ListInbox(userID int64) ([]domain.Message, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||
defer cancel()
|
||||
|
||||
const q = `
|
||||
q := `
|
||||
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
|
||||
FROM messages
|
||||
FROM users_messages
|
||||
WHERE user_id = ? AND is_archived = FALSE
|
||||
ORDER BY created_at DESC`
|
||||
rows, err := s.DB.QueryContext(s.rebind(ctx), s.bind(q), userID)
|
||||
q = s.bind(q)
|
||||
|
||||
rows, err := s.DB.QueryContext(ctx, q, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []accountMessageHandler.Message
|
||||
var out []domain.Message
|
||||
for rows.Next() {
|
||||
var m accountMessageHandler.Message
|
||||
var m domain.Message
|
||||
if err := rows.Scan(&m.ID, &m.From, &m.To, &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) ([]accountMessageHandler.Message, error) {
|
||||
func (s *Service) ListArchived(userID int64) ([]domain.Message, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||
defer cancel()
|
||||
|
||||
const q = `
|
||||
q := `
|
||||
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
|
||||
FROM messages
|
||||
FROM users_messages
|
||||
WHERE user_id = ? AND is_archived = TRUE
|
||||
ORDER BY created_at DESC`
|
||||
rows, err := s.DB.QueryContext(s.rebind(ctx), s.bind(q), userID)
|
||||
q = s.bind(q)
|
||||
|
||||
rows, err := s.DB.QueryContext(ctx, q, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []accountMessageHandler.Message
|
||||
var out []domain.Message
|
||||
for rows.Next() {
|
||||
var m accountMessageHandler.Message
|
||||
var m domain.Message
|
||||
if err := rows.Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -89,16 +94,18 @@ func (s *Service) ListArchived(userID int64) ([]accountMessageHandler.Message, e
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Service) GetByID(userID, id int64) (*accountMessageHandler.Message, error) {
|
||||
func (s *Service) GetByID(userID, id int64) (*domain.Message, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||
defer cancel()
|
||||
|
||||
const q = `
|
||||
q := `
|
||||
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
|
||||
FROM messages
|
||||
FROM users_messages
|
||||
WHERE user_id = ? AND id = ?`
|
||||
var m accountMessageHandler.Message
|
||||
err := s.DB.QueryRowContext(s.rebind(ctx), s.bind(q), userID, id).
|
||||
q = s.bind(q)
|
||||
|
||||
var m domain.Message
|
||||
err := s.DB.QueryRowContext(ctx, q, userID, id).
|
||||
Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
@@ -109,7 +116,7 @@ func (s *Service) GetByID(userID, id int64) (*accountMessageHandler.Message, err
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (s *Service) Create(userID int64, in accountMessageHandler.CreateMessageInput) (int64, error) {
|
||||
func (s *Service) Create(userID int64, in domain.CreateMessageInput) (int64, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||
defer cancel()
|
||||
|
||||
@@ -128,7 +135,7 @@ func (s *Service) Create(userID int64, in accountMessageHandler.CreateMessageInp
|
||||
const q = `
|
||||
INSERT INTO messages (user_id, from_email, to_email, subject, body, is_read, is_archived, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, FALSE, FALSE, CURRENT_TIMESTAMP)`
|
||||
res, err := s.DB.ExecContext(s.rebind(ctx), q, userID, "", in.To, in.Subject, in.Body)
|
||||
res, err := s.DB.ExecContext(ctx, q, userID, "", in.To, in.Subject, in.Body)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -136,17 +143,13 @@ func (s *Service) Create(userID int64, in accountMessageHandler.CreateMessageInp
|
||||
}
|
||||
}
|
||||
|
||||
// --- small helpers ---
|
||||
|
||||
// bind replaces ? with $1.. for Postgres if needed.
|
||||
// We keep queries written with ? for brevity and adapt here.
|
||||
// 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
|
||||
}
|
||||
// cheap replacer for positional params:
|
||||
n := 0
|
||||
out := make([]byte, 0, len(q)+10)
|
||||
out := make([]byte, 0, len(q)+8)
|
||||
for i := 0; i < len(q); i++ {
|
||||
if q[i] == '?' {
|
||||
n++
|
||||
@@ -159,9 +162,7 @@ func (s *Service) bind(q string) string {
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func (s *Service) rebind(ctx context.Context) context.Context { return ctx }
|
||||
|
||||
// intToStr avoids fmt for tiny helper
|
||||
// ToDo: helper dont think it should be here.
|
||||
func intToStr(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
// Package notifysvc
|
||||
// Path: /internal/platform/services/notifications
|
||||
// File: service.go
|
||||
// ToDo: carve out sql
|
||||
|
||||
package notifysvc
|
||||
|
||||
import (
|
||||
@@ -7,7 +11,7 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
accountNotificationHandler "synlotto-website/internal/handlers/account/notifications"
|
||||
domain "synlotto-website/internal/domain/notifications"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -31,7 +35,7 @@ func New(db *sql.DB, opts ...func(*Service)) *Service {
|
||||
func WithTimeout(d time.Duration) func(*Service) { return func(s *Service) { s.Timeout = d } }
|
||||
|
||||
// List returns newest-first notifications for a user.
|
||||
func (s *Service) List(userID int64) ([]accountNotificationHandler.Notification, error) {
|
||||
func (s *Service) List(userID int64) ([]domain.Notification, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||
defer cancel()
|
||||
|
||||
@@ -47,9 +51,9 @@ ORDER BY created_at DESC`
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []accountNotificationHandler.Notification
|
||||
var out []domain.Notification
|
||||
for rows.Next() {
|
||||
var n accountNotificationHandler.Notification
|
||||
var n domain.Notification
|
||||
if err := rows.Scan(&n.ID, &n.Title, &n.Body, &n.IsRead, &n.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -58,7 +62,7 @@ ORDER BY created_at DESC`
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Service) GetByID(userID, id int64) (*accountNotificationHandler.Notification, error) {
|
||||
func (s *Service) GetByID(userID, id int64) (*domain.Notification, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||
defer cancel()
|
||||
|
||||
@@ -67,7 +71,7 @@ SELECT id, title, body, is_read, created_at
|
||||
FROM notifications
|
||||
WHERE user_id = ? AND id = ?`
|
||||
|
||||
var n accountNotificationHandler.Notification
|
||||
var n domain.Notification
|
||||
err := s.DB.QueryRowContext(ctx, q, userID, id).
|
||||
Scan(&n.ID, &n.Title, &n.Body, &n.IsRead, &n.CreatedAt)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
|
||||
Reference in New Issue
Block a user