crossover

This commit is contained in:
KS Jannette
2026-02-27 15:07:43 -05:00
parent 0adfd70853
commit 5f81a4b1cc
31 changed files with 2890 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
package models
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/kjannette/koin-ping/backend-go/internal/domain"
)
type AddressModel struct {
pool *pgxpool.Pool
}
func NewAddressModel(pool *pgxpool.Pool) *AddressModel {
return &AddressModel{pool: pool}
}
func (m *AddressModel) Create(ctx context.Context, userID, address string, label *string) (*domain.Address, error) {
var a domain.Address
err := m.pool.QueryRow(ctx,
`INSERT INTO addresses (user_id, address, label)
VALUES ($1, $2, $3)
RETURNING id, user_id, address, label, created_at`,
userID, address, label,
).Scan(&a.ID, &a.UserID, &a.Address, &a.Label, &a.CreatedAt)
if err != nil {
return nil, err
}
return &a, nil
}
func (m *AddressModel) ListByUser(ctx context.Context, userID string) ([]domain.Address, error) {
rows, err := m.pool.Query(ctx,
`SELECT id, user_id, address, label, created_at
FROM addresses
WHERE user_id = $1
ORDER BY created_at DESC`,
userID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var addresses []domain.Address
for rows.Next() {
var a domain.Address
if err := rows.Scan(&a.ID, &a.UserID, &a.Address, &a.Label, &a.CreatedAt); err != nil {
return nil, err
}
addresses = append(addresses, a)
}
return addresses, rows.Err()
}
// ListAll returns all addresses system-wide (used by the poller).
func (m *AddressModel) ListAll(ctx context.Context) ([]domain.Address, error) {
rows, err := m.pool.Query(ctx,
`SELECT id, user_id, address, label, created_at
FROM addresses
ORDER BY created_at DESC`,
)
if err != nil {
return nil, err
}
defer rows.Close()
var addresses []domain.Address
for rows.Next() {
var a domain.Address
if err := rows.Scan(&a.ID, &a.UserID, &a.Address, &a.Label, &a.CreatedAt); err != nil {
return nil, err
}
addresses = append(addresses, a)
}
return addresses, rows.Err()
}
func (m *AddressModel) FindByID(ctx context.Context, id int, userID *string) (*domain.Address, error) {
var a domain.Address
var err error
if userID != nil {
err = m.pool.QueryRow(ctx,
`SELECT id, user_id, address, label, created_at
FROM addresses
WHERE id = $1 AND user_id = $2`,
id, *userID,
).Scan(&a.ID, &a.UserID, &a.Address, &a.Label, &a.CreatedAt)
} else {
err = m.pool.QueryRow(ctx,
`SELECT id, user_id, address, label, created_at
FROM addresses
WHERE id = $1`,
id,
).Scan(&a.ID, &a.UserID, &a.Address, &a.Label, &a.CreatedAt)
}
if err != nil {
if err.Error() == "no rows in result set" {
return nil, nil
}
return nil, err
}
return &a, nil
}
func (m *AddressModel) Remove(ctx context.Context, id int, userID string) (bool, error) {
tag, err := m.pool.Exec(ctx,
`DELETE FROM addresses WHERE id = $1 AND user_id = $2`,
id, userID,
)
if err != nil {
return false, err
}
return tag.RowsAffected() > 0, nil
}

View File

@@ -0,0 +1,81 @@
package models
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/kjannette/koin-ping/backend-go/internal/domain"
)
type AlertEventModel struct {
pool *pgxpool.Pool
}
func NewAlertEventModel(pool *pgxpool.Pool) *AlertEventModel {
return &AlertEventModel{pool: pool}
}
func (m *AlertEventModel) ListRecentByUser(ctx context.Context, userID string, limit int) ([]domain.AlertEvent, error) {
rows, err := m.pool.Query(ctx,
`SELECT ae.id, ae.alert_rule_id, ae.message, ae.address_label, ae.tx_hash, ae.timestamp
FROM alert_events ae
JOIN alert_rules ar ON ar.id = ae.alert_rule_id
JOIN addresses a ON a.id = ar.address_id
WHERE a.user_id = $1
ORDER BY ae.timestamp DESC
LIMIT $2`,
userID, limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var events []domain.AlertEvent
for rows.Next() {
var e domain.AlertEvent
if err := rows.Scan(&e.ID, &e.AlertRuleID, &e.Message, &e.AddressLabel, &e.TxHash, &e.Timestamp); err != nil {
return nil, err
}
events = append(events, e)
}
return events, rows.Err()
}
func (m *AlertEventModel) ListRecent(ctx context.Context, limit int) ([]domain.AlertEvent, error) {
rows, err := m.pool.Query(ctx,
`SELECT id, alert_rule_id, message, address_label, tx_hash, timestamp
FROM alert_events
ORDER BY timestamp DESC
LIMIT $1`,
limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var events []domain.AlertEvent
for rows.Next() {
var e domain.AlertEvent
if err := rows.Scan(&e.ID, &e.AlertRuleID, &e.Message, &e.AddressLabel, &e.TxHash, &e.Timestamp); err != nil {
return nil, err
}
events = append(events, e)
}
return events, rows.Err()
}
func (m *AlertEventModel) Create(ctx context.Context, alertRuleID int, message string, addressLabel *string, txHash *string) (*domain.AlertEvent, error) {
var e domain.AlertEvent
err := m.pool.QueryRow(ctx,
`INSERT INTO alert_events (alert_rule_id, message, address_label, tx_hash)
VALUES ($1, $2, $3, $4)
RETURNING id, alert_rule_id, message, address_label, tx_hash, timestamp`,
alertRuleID, message, addressLabel, txHash,
).Scan(&e.ID, &e.AlertRuleID, &e.Message, &e.AddressLabel, &e.TxHash, &e.Timestamp)
if err != nil {
return nil, err
}
return &e, nil
}

View File

@@ -0,0 +1,113 @@
package models
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/kjannette/koin-ping/backend-go/internal/domain"
)
type AlertRuleModel struct {
pool *pgxpool.Pool
}
func NewAlertRuleModel(pool *pgxpool.Pool) *AlertRuleModel {
return &AlertRuleModel{pool: pool}
}
func (m *AlertRuleModel) Create(ctx context.Context, addressID int, alertType domain.AlertType, threshold *float64) (*domain.AlertRule, error) {
var r domain.AlertRule
err := m.pool.QueryRow(ctx,
`INSERT INTO alert_rules (address_id, type, threshold, enabled)
VALUES ($1, $2, $3, TRUE)
RETURNING id, address_id, type, threshold, enabled, created_at`,
addressID, string(alertType), threshold,
).Scan(&r.ID, &r.AddressID, &r.Type, &r.Threshold, &r.Enabled, &r.CreatedAt)
if err != nil {
return nil, err
}
return &r, nil
}
func (m *AlertRuleModel) ListByAddress(ctx context.Context, addressID int) ([]domain.AlertRule, error) {
rows, err := m.pool.Query(ctx,
`SELECT id, address_id, type, threshold, enabled, created_at
FROM alert_rules
WHERE address_id = $1
ORDER BY created_at DESC`,
addressID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var rules []domain.AlertRule
for rows.Next() {
var r domain.AlertRule
if err := rows.Scan(&r.ID, &r.AddressID, &r.Type, &r.Threshold, &r.Enabled, &r.CreatedAt); err != nil {
return nil, err
}
rules = append(rules, r)
}
return rules, rows.Err()
}
func (m *AlertRuleModel) FindByID(ctx context.Context, id int, userID *string) (*domain.AlertRule, error) {
var r domain.AlertRule
var err error
if userID != nil {
err = m.pool.QueryRow(ctx,
`SELECT ar.id, ar.address_id, ar.type, ar.threshold, ar.enabled, ar.created_at
FROM alert_rules ar
JOIN addresses a ON a.id = ar.address_id
WHERE ar.id = $1 AND a.user_id = $2`,
id, *userID,
).Scan(&r.ID, &r.AddressID, &r.Type, &r.Threshold, &r.Enabled, &r.CreatedAt)
} else {
err = m.pool.QueryRow(ctx,
`SELECT id, address_id, type, threshold, enabled, created_at
FROM alert_rules
WHERE id = $1`,
id,
).Scan(&r.ID, &r.AddressID, &r.Type, &r.Threshold, &r.Enabled, &r.CreatedAt)
}
if err != nil {
if err.Error() == "no rows in result set" {
return nil, nil
}
return nil, err
}
return &r, nil
}
func (m *AlertRuleModel) UpdateEnabled(ctx context.Context, id int, enabled bool) (*domain.AlertRule, error) {
var r domain.AlertRule
err := m.pool.QueryRow(ctx,
`UPDATE alert_rules
SET enabled = $2
WHERE id = $1
RETURNING id, address_id, type, threshold, enabled, created_at`,
id, enabled,
).Scan(&r.ID, &r.AddressID, &r.Type, &r.Threshold, &r.Enabled, &r.CreatedAt)
if err != nil {
if err.Error() == "no rows in result set" {
return nil, nil
}
return nil, err
}
return &r, nil
}
func (m *AlertRuleModel) Remove(ctx context.Context, id int) (bool, error) {
tag, err := m.pool.Exec(ctx,
`DELETE FROM alert_rules WHERE id = $1`,
id,
)
if err != nil {
return false, err
}
return tag.RowsAffected() > 0, nil
}

View File

@@ -0,0 +1,84 @@
package models
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/kjannette/koin-ping/backend-go/internal/domain"
)
type CheckpointModel struct {
pool *pgxpool.Pool
}
func NewCheckpointModel(pool *pgxpool.Pool) *CheckpointModel {
return &CheckpointModel{pool: pool}
}
// GetLastCheckedBlock returns the last checked block for an address, or -1 if never checked.
func (m *CheckpointModel) GetLastCheckedBlock(ctx context.Context, addressID int) (int, bool, error) {
var block int
err := m.pool.QueryRow(ctx,
`SELECT last_checked_block FROM address_checkpoints WHERE address_id = $1`,
addressID,
).Scan(&block)
if err != nil {
if err.Error() == "no rows in result set" {
return 0, false, nil
}
return 0, false, err
}
return block, true, nil
}
func (m *CheckpointModel) UpdateLastCheckedBlock(ctx context.Context, addressID, blockNumber int) (*domain.AddressCheckpoint, error) {
var cp domain.AddressCheckpoint
err := m.pool.QueryRow(ctx,
`INSERT INTO address_checkpoints (address_id, last_checked_block, last_checked_at)
VALUES ($1, $2, NOW())
ON CONFLICT (address_id)
DO UPDATE SET
last_checked_block = $2,
last_checked_at = NOW()
RETURNING address_id, last_checked_block, last_checked_at`,
addressID, blockNumber,
).Scan(&cp.AddressID, &cp.LastCheckedBlock, &cp.LastCheckedAt)
if err != nil {
return nil, err
}
return &cp, nil
}
func (m *CheckpointModel) ListAll(ctx context.Context) ([]domain.CheckpointDetail, error) {
rows, err := m.pool.Query(ctx,
`SELECT ac.address_id, a.address, a.label, ac.last_checked_block, ac.last_checked_at
FROM address_checkpoints ac
JOIN addresses a ON a.id = ac.address_id
ORDER BY ac.last_checked_at DESC`,
)
if err != nil {
return nil, err
}
defer rows.Close()
var details []domain.CheckpointDetail
for rows.Next() {
var d domain.CheckpointDetail
if err := rows.Scan(&d.AddressID, &d.Address, &d.Label, &d.LastCheckedBlock, &d.LastCheckedAt); err != nil {
return nil, err
}
details = append(details, d)
}
return details, rows.Err()
}
func (m *CheckpointModel) Remove(ctx context.Context, addressID int) (bool, error) {
tag, err := m.pool.Exec(ctx,
`DELETE FROM address_checkpoints WHERE address_id = $1`,
addressID,
)
if err != nil {
return false, err
}
return tag.RowsAffected() > 0, nil
}

View File

@@ -0,0 +1,95 @@
package models
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/kjannette/koin-ping/backend-go/internal/domain"
)
type NotificationConfigModel struct {
pool *pgxpool.Pool
}
func NewNotificationConfigModel(pool *pgxpool.Pool) *NotificationConfigModel {
return &NotificationConfigModel{pool: pool}
}
func (m *NotificationConfigModel) GetConfig(ctx context.Context, userID string) (*domain.NotificationConfig, error) {
var c domain.NotificationConfig
err := m.pool.QueryRow(ctx,
`SELECT user_id, discord_webhook_url, telegram_chat_id, telegram_bot_token,
email, notification_enabled, created_at, updated_at
FROM user_notification_configs
WHERE user_id = $1`,
userID,
).Scan(&c.UserID, &c.DiscordWebhookURL, &c.TelegramChatID, &c.TelegramBotToken,
&c.Email, &c.NotificationEnabled, &c.CreatedAt, &c.UpdatedAt)
if err != nil {
if err.Error() == "no rows in result set" {
return nil, nil
}
return nil, err
}
return &c, nil
}
func (m *NotificationConfigModel) UpsertConfig(ctx context.Context, userID string, cfg domain.NotificationConfig) (*domain.NotificationConfig, error) {
var c domain.NotificationConfig
err := m.pool.QueryRow(ctx,
`INSERT INTO user_notification_configs
(user_id, discord_webhook_url, telegram_chat_id, telegram_bot_token, email, notification_enabled, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW())
ON CONFLICT (user_id)
DO UPDATE SET
discord_webhook_url = COALESCE($2, user_notification_configs.discord_webhook_url),
telegram_chat_id = COALESCE($3, user_notification_configs.telegram_chat_id),
telegram_bot_token = COALESCE($4, user_notification_configs.telegram_bot_token),
email = COALESCE($5, user_notification_configs.email),
notification_enabled = $6,
updated_at = NOW()
RETURNING user_id, discord_webhook_url, telegram_chat_id, telegram_bot_token,
email, notification_enabled, created_at, updated_at`,
userID, cfg.DiscordWebhookURL, cfg.TelegramChatID, cfg.TelegramBotToken,
cfg.Email, cfg.NotificationEnabled,
).Scan(&c.UserID, &c.DiscordWebhookURL, &c.TelegramChatID, &c.TelegramBotToken,
&c.Email, &c.NotificationEnabled, &c.CreatedAt, &c.UpdatedAt)
if err != nil {
return nil, err
}
return &c, nil
}
func (m *NotificationConfigModel) Remove(ctx context.Context, userID string) (bool, error) {
tag, err := m.pool.Exec(ctx,
`DELETE FROM user_notification_configs WHERE user_id = $1`,
userID,
)
if err != nil {
return false, err
}
return tag.RowsAffected() > 0, nil
}
func (m *NotificationConfigModel) ListEnabled(ctx context.Context) ([]domain.NotificationConfig, error) {
rows, err := m.pool.Query(ctx,
`SELECT user_id, discord_webhook_url, telegram_chat_id, email
FROM user_notification_configs
WHERE notification_enabled = TRUE`,
)
if err != nil {
return nil, err
}
defer rows.Close()
var configs []domain.NotificationConfig
for rows.Next() {
var c domain.NotificationConfig
if err := rows.Scan(&c.UserID, &c.DiscordWebhookURL, &c.TelegramChatID, &c.Email); err != nil {
return nil, err
}
c.NotificationEnabled = true
configs = append(configs, c)
}
return configs, rows.Err()
}