package notificationstore import ( "context" "database/sql" "errors" "fmt" "galaxy/notification/internal/api/intentstream" "galaxy/notification/internal/service/acceptintent" ) // Compile-time confirmation that *Store satisfies acceptintent.Store. The // runtime wiring depends on this so the accept-intent service can consume // the PostgreSQL adapter directly. var _ acceptintent.Store = (*Store)(nil) // CreateAcceptance writes one notification record together with its derived // route slots inside one BEGIN … COMMIT transaction. Idempotency races // surface as `acceptintent.ErrConflict`. func (store *Store) CreateAcceptance(ctx context.Context, input acceptintent.CreateAcceptanceInput) error { if store == nil { return errors.New("create notification acceptance: nil store") } if ctx == nil { return errors.New("create notification acceptance: nil context") } if err := input.Validate(); err != nil { return fmt.Errorf("create notification acceptance: %w", err) } return store.withTx(ctx, "create notification acceptance", func(ctx context.Context, tx *sql.Tx) error { if err := insertRecord(ctx, tx, input.Notification, input.Idempotency.ExpiresAt); err != nil { if isUniqueViolation(err) { return acceptintent.ErrConflict } return fmt.Errorf("create notification acceptance: insert record: %w", err) } for index, route := range input.Routes { if err := insertRoute(ctx, tx, route); err != nil { return fmt.Errorf("create notification acceptance: insert route[%d]: %w", index, err) } } return nil }) } // GetIdempotency loads one accepted idempotency reservation. Because the // records row IS the idempotency reservation, the lookup keys on // `(producer, idempotency_key)` and projects the relevant subset of the row // into an IdempotencyRecord. func (store *Store) GetIdempotency(ctx context.Context, producer intentstream.Producer, idempotencyKey string) (acceptintent.IdempotencyRecord, bool, error) { if store == nil { return acceptintent.IdempotencyRecord{}, false, errors.New("get notification idempotency: nil store") } if ctx == nil { return acceptintent.IdempotencyRecord{}, false, errors.New("get notification idempotency: nil context") } operationCtx, cancel, err := store.operationContext(ctx, "get notification idempotency") if err != nil { return acceptintent.IdempotencyRecord{}, false, err } defer cancel() scanned, found, err := loadIdempotencyByKey(operationCtx, store.db, string(producer), idempotencyKey) if err != nil { return acceptintent.IdempotencyRecord{}, false, err } if !found { return acceptintent.IdempotencyRecord{}, false, nil } return idempotencyRecordFromScanned(scanned), true, nil } // GetNotification loads one accepted notification by NotificationID. func (store *Store) GetNotification(ctx context.Context, notificationID string) (acceptintent.NotificationRecord, bool, error) { if store == nil { return acceptintent.NotificationRecord{}, false, errors.New("get notification record: nil store") } if ctx == nil { return acceptintent.NotificationRecord{}, false, errors.New("get notification record: nil context") } operationCtx, cancel, err := store.operationContext(ctx, "get notification record") if err != nil { return acceptintent.NotificationRecord{}, false, err } defer cancel() scanned, found, err := loadRecord(operationCtx, store.db, notificationID) if err != nil { return acceptintent.NotificationRecord{}, false, err } if !found { return acceptintent.NotificationRecord{}, false, nil } return scanned.Record, true, nil } // GetRoute loads one accepted notification route by `(notificationID, // routeID)`. Required by the publisher worker contracts. func (store *Store) GetRoute(ctx context.Context, notificationID string, routeID string) (acceptintent.NotificationRoute, bool, error) { if store == nil { return acceptintent.NotificationRoute{}, false, errors.New("get notification route: nil store") } if ctx == nil { return acceptintent.NotificationRoute{}, false, errors.New("get notification route: nil context") } operationCtx, cancel, err := store.operationContext(ctx, "get notification route") if err != nil { return acceptintent.NotificationRoute{}, false, err } defer cancel() return loadRoute(operationCtx, store.db, notificationID, routeID) }