249 lines
7.4 KiB
Go
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(
|
|
¬ificationID,
|
|
&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
|
|
}
|