feat: use postgres
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user