LINUS ROUND 1: Delete dead code (rust/c), generic Store[T], tests, slim main.go
- Removed rust-service/, c-runtime/, kafka stubs - Generic Store[T] pattern with real tests - Slimmed main.go from 324 to ~50 lines - Added config, middleware, store, ledger, pdf tests - Frontend SPA shell with router - Binary: 15.5MB -> 12MB
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
// Package store provides a generic CRUD repository for BOC entities.
|
||||
// Linus principle: write it once, use it everywhere.
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// DB wraps sql.DB with helper methods
|
||||
type DB struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
// New wraps an existing sql.DB
|
||||
func New(db *sql.DB) *DB {
|
||||
return &DB{db}
|
||||
}
|
||||
|
||||
// WithTx executes fn inside a transaction. Commits on nil error, rolls back on error.
|
||||
func (db *DB) WithTx(ctx context.Context, fn func(*sql.Tx) error) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
if err := fn(tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// Scanner knows how to scan a database row into itself
|
||||
type Scanner interface {
|
||||
ScanRow(*sql.Rows) error
|
||||
}
|
||||
|
||||
// Scanners knows how to scan a single row
|
||||
type Scanners interface {
|
||||
ScanRow(*sql.Row) error
|
||||
}
|
||||
|
||||
// Store provides generic CRUD for a table.
|
||||
// T must implement Scanner for List and Scanners for Get.
|
||||
type Store[T Scanner] struct {
|
||||
db *DB
|
||||
table string
|
||||
columns []string
|
||||
scanFn func(*sql.Rows) (T, error)
|
||||
scanOneFn func(*sql.Row) (T, error)
|
||||
}
|
||||
|
||||
// NewStore creates a Store for the given table and columns.
|
||||
func NewStore[T Scanner](db *DB, table string, columns []string,
|
||||
scanFn func(*sql.Rows) (T, error),
|
||||
scanOneFn func(*sql.Row) (T, error)) *Store[T] {
|
||||
return &Store[T]{
|
||||
db: db,
|
||||
table: table,
|
||||
columns: columns,
|
||||
scanFn: scanFn,
|
||||
scanOneFn: scanOneFn,
|
||||
}
|
||||
}
|
||||
|
||||
// List returns all rows matching the where clause
|
||||
func (s *Store[T]) List(ctx context.Context, where string, args ...interface{}) ([]T, error) {
|
||||
query := fmt.Sprintf("SELECT %s FROM %s", strings.Join(s.columns, ", "), s.table)
|
||||
if where != "" {
|
||||
query += " WHERE " + where
|
||||
}
|
||||
query += " ORDER BY created_at DESC LIMIT 100"
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list %s: %w", s.table, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var results []T
|
||||
for rows.Next() {
|
||||
item, err := s.scanFn(rows)
|
||||
if err != nil {
|
||||
continue // skip bad rows, log in production
|
||||
}
|
||||
results = append(results, item)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Get returns a single row by ID
|
||||
func (s *Store[T]) Get(ctx context.Context, id string) (T, error) {
|
||||
var zero T
|
||||
query := fmt.Sprintf("SELECT %s FROM %s WHERE id = $1", strings.Join(s.columns, ", "), s.table)
|
||||
row := s.db.QueryRowContext(ctx, query, id)
|
||||
item, err := s.scanOneFn(row)
|
||||
if err == sql.ErrNoRows {
|
||||
return zero, fmt.Errorf("%s not found", s.table)
|
||||
}
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("get %s: %w", s.table, err)
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// Delete removes a row by ID
|
||||
func (s *Store[T]) Delete(ctx context.Context, id string) error {
|
||||
query := fmt.Sprintf("DELETE FROM %s WHERE id = $1", s.table)
|
||||
_, err := s.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete %s: %w", s.table, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper: pqArray handles nil slices
|
||||
func pqArray(a []string) interface{} {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
return pq.Array(a)
|
||||
}
|
||||
|
||||
// Helper: now returns current time
|
||||
func now() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
// Helper: isZero checks if a value is zero
|
||||
func isZero(v interface{}) bool {
|
||||
return reflect.ValueOf(v).IsZero()
|
||||
}
|
||||
Reference in New Issue
Block a user