First slice of the inventory-service port, running in parallel READ-ONLY against the production inventory_db (never written): - main.go/store.go: pgx pool (forced read-only), enum-DB loader extracting AttributeSetInfo for set-name resolution, /health, /sets/list, /characters/list. - Dockerfile + compose service inventory-go (127.0.0.1:8772, enum JSON mounted). Validated vs the Python service on the same DB: /characters/list 167 chars exact counts; /sets/list 76 sets EXACT match (ids, names, counts). Remaining (large): /search/items (40+ filters + enrich_db_item), inventory fetch, item-processing ingestion (extract_item_properties), and the suitbuilder solver. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
83 lines
2.1 KiB
Go
83 lines
2.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// newPool creates a pgx pool. When readOnly (the default in parallel mode), every
|
|
// connection is forced into read-only transactions so the Go service can never
|
|
// mutate the production inventory_db it shares with the Python service.
|
|
func newPool(ctx context.Context, dsn string, readOnly bool) (*pgxpool.Pool, error) {
|
|
cfg, err := pgxpool.ParseConfig(dsn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse DATABASE_URL: %w", err)
|
|
}
|
|
cfg.MaxConns = 10
|
|
cfg.MaxConnIdleTime = 5 * time.Minute
|
|
if readOnly {
|
|
cfg.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
|
|
_, err := conn.Exec(ctx, "SET default_transaction_read_only = on")
|
|
return err
|
|
}
|
|
}
|
|
return pgxpool.NewWithConfig(ctx, cfg)
|
|
}
|
|
|
|
func queryRowsAsMaps(ctx context.Context, pool *pgxpool.Pool, sql string, args ...any) ([]map[string]any, error) {
|
|
rows, err := pool.Query(ctx, sql, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out, err := pgx.CollectRows(rows, pgx.RowToMap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if out == nil {
|
|
out = []map[string]any{}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// pyISO mirrors Python datetime.isoformat() for a UTC value (matches FastAPI's
|
|
// jsonable_encoder). Note the inventory-service stores naive datetimes (no tz),
|
|
// so isoformat has no offset — we format without one.
|
|
func pyISO(t time.Time) string {
|
|
t = t.UTC()
|
|
if t.Nanosecond() == 0 {
|
|
return t.Format("2006-01-02T15:04:05")
|
|
}
|
|
return t.Format("2006-01-02T15:04:05") + fmt.Sprintf(".%06d", t.Nanosecond()/1000)
|
|
}
|
|
|
|
func formatTimes(rows []map[string]any, keys ...string) {
|
|
for _, m := range rows {
|
|
for _, k := range keys {
|
|
if t, ok := m[k].(time.Time); ok {
|
|
m[k] = pyISO(t)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func toStr(v any) string {
|
|
if s, ok := v.(string); ok {
|
|
return s
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
if err := json.NewEncoder(w).Encode(v); err != nil {
|
|
slog.Error("json encode failed", "err", err)
|
|
}
|
|
}
|