Files
galaxy-game/notification/internal/adapters/postgres/notificationstore/routes.go
T
2026-04-26 20:34:39 +02:00

249 lines
7.4 KiB
Go

package notificationstore
import (
"context"
"database/sql"
"fmt"
"time"
"galaxy/notification/internal/api/intentstream"
pgtable "galaxy/notification/internal/adapters/postgres/jet/notification/table"
"galaxy/notification/internal/service/acceptintent"
pg "github.com/go-jet/jet/v2/postgres"
)
// routeSelectColumns is the canonical SELECT list for the routes table,
// matching scanRoute's column order.
var routeSelectColumns = pg.ColumnList{
pgtable.Routes.NotificationID,
pgtable.Routes.RouteID,
pgtable.Routes.Channel,
pgtable.Routes.RecipientRef,
pgtable.Routes.Status,
pgtable.Routes.AttemptCount,
pgtable.Routes.MaxAttempts,
pgtable.Routes.NextAttemptAt,
pgtable.Routes.ResolvedEmail,
pgtable.Routes.ResolvedLocale,
pgtable.Routes.LastErrorClassification,
pgtable.Routes.LastErrorMessage,
pgtable.Routes.LastErrorAt,
pgtable.Routes.CreatedAt,
pgtable.Routes.UpdatedAt,
pgtable.Routes.PublishedAt,
pgtable.Routes.DeadLetteredAt,
pgtable.Routes.SkippedAt,
}
// scanRoute scans one routes row from rs.
func scanRoute(rs rowScanner) (acceptintent.NotificationRoute, error) {
var (
notificationID string
routeID string
channel string
recipientRef string
status string
attemptCount int
maxAttempts int
nextAttemptAt sql.NullTime
resolvedEmail string
resolvedLocale string
lastErrorClassification string
lastErrorMessage string
lastErrorAt sql.NullTime
createdAt time.Time
updatedAt time.Time
publishedAt sql.NullTime
deadLetteredAt sql.NullTime
skippedAt sql.NullTime
)
if err := rs.Scan(
&notificationID,
&routeID,
&channel,
&recipientRef,
&status,
&attemptCount,
&maxAttempts,
&nextAttemptAt,
&resolvedEmail,
&resolvedLocale,
&lastErrorClassification,
&lastErrorMessage,
&lastErrorAt,
&createdAt,
&updatedAt,
&publishedAt,
&deadLetteredAt,
&skippedAt,
); err != nil {
return acceptintent.NotificationRoute{}, err
}
return acceptintent.NotificationRoute{
NotificationID: notificationID,
RouteID: routeID,
Channel: intentstream.Channel(channel),
RecipientRef: recipientRef,
Status: acceptintent.RouteStatus(status),
AttemptCount: attemptCount,
MaxAttempts: maxAttempts,
NextAttemptAt: timeFromNullable(nextAttemptAt),
ResolvedEmail: resolvedEmail,
ResolvedLocale: resolvedLocale,
LastErrorClassification: lastErrorClassification,
LastErrorMessage: lastErrorMessage,
LastErrorAt: timeFromNullable(lastErrorAt),
CreatedAt: createdAt.UTC(),
UpdatedAt: updatedAt.UTC(),
PublishedAt: timeFromNullable(publishedAt),
DeadLetteredAt: timeFromNullable(deadLetteredAt),
SkippedAt: timeFromNullable(skippedAt),
}, nil
}
// insertRoute writes one route row inside an open transaction.
func insertRoute(ctx context.Context, tx *sql.Tx, route acceptintent.NotificationRoute) error {
if err := route.Validate(); err != nil {
return fmt.Errorf("insert route: %w", err)
}
stmt := pgtable.Routes.INSERT(
pgtable.Routes.NotificationID,
pgtable.Routes.RouteID,
pgtable.Routes.Channel,
pgtable.Routes.RecipientRef,
pgtable.Routes.Status,
pgtable.Routes.AttemptCount,
pgtable.Routes.MaxAttempts,
pgtable.Routes.NextAttemptAt,
pgtable.Routes.ResolvedEmail,
pgtable.Routes.ResolvedLocale,
pgtable.Routes.LastErrorClassification,
pgtable.Routes.LastErrorMessage,
pgtable.Routes.LastErrorAt,
pgtable.Routes.CreatedAt,
pgtable.Routes.UpdatedAt,
pgtable.Routes.PublishedAt,
pgtable.Routes.DeadLetteredAt,
pgtable.Routes.SkippedAt,
).VALUES(
route.NotificationID,
route.RouteID,
string(route.Channel),
route.RecipientRef,
string(route.Status),
route.AttemptCount,
route.MaxAttempts,
nullableTime(route.NextAttemptAt),
route.ResolvedEmail,
route.ResolvedLocale,
route.LastErrorClassification,
route.LastErrorMessage,
nullableTime(route.LastErrorAt),
route.CreatedAt.UTC(),
route.UpdatedAt.UTC(),
nullableTime(route.PublishedAt),
nullableTime(route.DeadLetteredAt),
nullableTime(route.SkippedAt),
)
query, args := stmt.Sql()
if _, err := tx.ExecContext(ctx, query, args...); err != nil {
return err
}
return nil
}
// loadRoute returns one route row by composite key. found is false when no
// matching row exists.
func loadRoute(ctx context.Context, db *sql.DB, notificationID string, routeID string) (acceptintent.NotificationRoute, bool, error) {
stmt := pg.SELECT(routeSelectColumns).
FROM(pgtable.Routes).
WHERE(pg.AND(
pgtable.Routes.NotificationID.EQ(pg.String(notificationID)),
pgtable.Routes.RouteID.EQ(pg.String(routeID)),
))
query, args := stmt.Sql()
row := db.QueryRowContext(ctx, query, args...)
route, err := scanRoute(row)
if isNoRows(err) {
return acceptintent.NotificationRoute{}, false, nil
}
if err != nil {
return acceptintent.NotificationRoute{}, false, fmt.Errorf("load notification route: %w", err)
}
return route, true, nil
}
// loadRouteTx returns one route row by composite key inside an open
// transaction.
func loadRouteTx(ctx context.Context, tx *sql.Tx, notificationID string, routeID string) (acceptintent.NotificationRoute, bool, error) {
stmt := pg.SELECT(routeSelectColumns).
FROM(pgtable.Routes).
WHERE(pg.AND(
pgtable.Routes.NotificationID.EQ(pg.String(notificationID)),
pgtable.Routes.RouteID.EQ(pg.String(routeID)),
))
query, args := stmt.Sql()
row := tx.QueryRowContext(ctx, query, args...)
route, err := scanRoute(row)
if isNoRows(err) {
return acceptintent.NotificationRoute{}, false, nil
}
if err != nil {
return acceptintent.NotificationRoute{}, false, fmt.Errorf("load notification route: %w", err)
}
return route, true, nil
}
// updateRouteIfMatching writes the route columns back inside an open
// transaction, gated on `updated_at = expectedUpdatedAt`. Returns the
// number of rows actually updated; zero indicates an optimistic-concurrency
// loss.
func updateRouteIfMatching(ctx context.Context, tx *sql.Tx, route acceptintent.NotificationRoute, expectedUpdatedAt time.Time) (int64, error) {
stmt := pgtable.Routes.UPDATE(
pgtable.Routes.Status,
pgtable.Routes.AttemptCount,
pgtable.Routes.NextAttemptAt,
pgtable.Routes.ResolvedEmail,
pgtable.Routes.ResolvedLocale,
pgtable.Routes.LastErrorClassification,
pgtable.Routes.LastErrorMessage,
pgtable.Routes.LastErrorAt,
pgtable.Routes.UpdatedAt,
pgtable.Routes.PublishedAt,
pgtable.Routes.DeadLetteredAt,
pgtable.Routes.SkippedAt,
).SET(
string(route.Status),
route.AttemptCount,
nullableTime(route.NextAttemptAt),
route.ResolvedEmail,
route.ResolvedLocale,
route.LastErrorClassification,
route.LastErrorMessage,
nullableTime(route.LastErrorAt),
route.UpdatedAt.UTC(),
nullableTime(route.PublishedAt),
nullableTime(route.DeadLetteredAt),
nullableTime(route.SkippedAt),
).WHERE(pg.AND(
pgtable.Routes.NotificationID.EQ(pg.String(route.NotificationID)),
pgtable.Routes.RouteID.EQ(pg.String(route.RouteID)),
pgtable.Routes.UpdatedAt.EQ(pg.TimestampzT(expectedUpdatedAt.UTC())),
))
query, args := stmt.Sql()
result, err := tx.ExecContext(ctx, query, args...)
if err != nil {
return 0, err
}
rows, err := result.RowsAffected()
if err != nil {
return 0, err
}
return rows, nil
}