diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 363efcc..94124bb 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -5,7 +5,10 @@ "Bash(go build *)", "Bash(go run *)", "Bash(git add *)", - "Bash(git commit *)" + "Bash(git commit *)", + "Bash(go vet *)", + "Bash(/Users/ryan/.claude/plugins/cache/superpowers-marketplace/superpowers/6.0.3/skills/subagent-driven-development/scripts/task-brief *)", + "Bash(go get *)" ] } } diff --git a/go.mod b/go.mod index 4a71274..511f686 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,14 @@ module aiaa-notification-service go 1.26.2 -require github.com/spf13/viper v1.21.0 +require ( + github.com/go-sql-driver/mysql v1.10.0 + github.com/jmoiron/sqlx v1.4.0 + github.com/spf13/viper v1.21.0 +) require ( + filippo.io/edwards25519 v1.2.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect diff --git a/go.sum b/go.sum index 81f59c4..e800caf 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,29 @@ +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= +github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/internal/store/mysql.go b/internal/store/mysql.go new file mode 100644 index 0000000..94fa20e --- /dev/null +++ b/internal/store/mysql.go @@ -0,0 +1,28 @@ +package store + +import ( + "fmt" + + "aiaa-notification-service/internal/config" + + "github.com/jmoiron/sqlx" + _ "github.com/go-sql-driver/mysql" +) + +type Store struct { + DB *sqlx.DB +} + +func NewStore(cfg config.DatabaseConfig) (*Store, error) { + db, err := sqlx.Connect("mysql", cfg.DSN()) + if err != nil { + return nil, fmt.Errorf("connect mysql: %w", err) + } + db.SetMaxOpenConns(25) + db.SetMaxIdleConns(5) + return &Store{DB: db}, nil +} + +func (s *Store) Close() error { + return s.DB.Close() +} diff --git a/internal/store/source.go b/internal/store/source.go new file mode 100644 index 0000000..49380f7 --- /dev/null +++ b/internal/store/source.go @@ -0,0 +1,81 @@ +package store + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + + "aiaa-notification-service/internal/model" +) + +func generateAPIKey() string { + b := make([]byte, 32) + rand.Read(b) + return "sk-" + hex.EncodeToString(b) +} + +func (s *Store) CreateSource(ctx context.Context, src *model.Source) error { + src.APIKey = generateAPIKey() + query := `INSERT INTO source (name, api_key, parse_mode, parse_pattern, status) VALUES (?, ?, ?, ?, ?)` + result, err := s.DB.ExecContext(ctx, query, src.Name, src.APIKey, src.ParseMode, src.ParsePattern, src.Status) + if err != nil { + return fmt.Errorf("create source: %w", err) + } + id, _ := result.LastInsertId() + src.ID = int(id) + return nil +} + +func (s *Store) GetSource(ctx context.Context, id int) (*model.Source, error) { + var src model.Source + err := s.DB.GetContext(ctx, &src, `SELECT * FROM source WHERE id = ?`, id) + if err != nil { + return nil, fmt.Errorf("get source %d: %w", id, err) + } + return &src, nil +} + +func (s *Store) GetSourceByAPIKey(ctx context.Context, apiKey string) (*model.Source, error) { + var src model.Source + err := s.DB.GetContext(ctx, &src, `SELECT * FROM source WHERE api_key = ? AND status = 1`, apiKey) + if err != nil { + return nil, fmt.Errorf("get source by api_key: %w", err) + } + return &src, nil +} + +func (s *Store) GetSourceByName(ctx context.Context, name string) (*model.Source, error) { + var src model.Source + err := s.DB.GetContext(ctx, &src, `SELECT * FROM source WHERE name = ?`, name) + if err != nil { + return nil, fmt.Errorf("get source by name %s: %w", name, err) + } + return &src, nil +} + +func (s *Store) ListSources(ctx context.Context) ([]model.Source, error) { + var sources []model.Source + err := s.DB.SelectContext(ctx, &sources, `SELECT * FROM source ORDER BY id`) + if err != nil { + return nil, fmt.Errorf("list sources: %w", err) + } + return sources, nil +} + +func (s *Store) UpdateSource(ctx context.Context, id int, src *model.Source) error { + query := `UPDATE source SET name=?, parse_mode=?, parse_pattern=?, status=? WHERE id=?` + _, err := s.DB.ExecContext(ctx, query, src.Name, src.ParseMode, src.ParsePattern, src.Status, id) + if err != nil { + return fmt.Errorf("update source %d: %w", id, err) + } + return nil +} + +func (s *Store) DeleteSource(ctx context.Context, id int) error { + _, err := s.DB.ExecContext(ctx, `DELETE FROM source WHERE id = ?`, id) + if err != nil { + return fmt.Errorf("delete source %d: %w", id, err) + } + return nil +}