Files
miti99bot/internal/migration/cloudflare_d1_client.go
T
tiennm99 0e6f2d0a1f fix(migration): satisfy errcheck on Close + Fprint* in migration toolchain
Pre-existing 9 errcheck violations introduced by 1a8da2d blocked
golangci-lint on every CI run after the migration toolchain landed.

- Wrap defer Close() in anonymous func to match house style
  (cf. internal/modules/lolschedule/api_client.go:162).
- Mark Fprint*/Fprintln to io.Writer with _, _ = (best-effort writes
  to os.Stdout / bytes.Buffer; errors not actionable for callers).

No behavior change. go vet, golangci-lint, go test ./... all clean
locally.
2026-05-16 11:02:15 +07:00

93 lines
2.7 KiB
Go

package migration
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// CloudflareD1Client is a thin read-only REST client for the legacy
// Cloudflare D1 database. The migration only uses it for the optional
// trading_trades audit dump in Phase 03; runtime data flow never reads D1.
type CloudflareD1Client struct {
httpClient *http.Client
apiBase string
accountID string
databaseID string
apiToken string
}
func NewCloudflareD1Client(accountID, databaseID, apiToken string) *CloudflareD1Client {
if accountID == "" || databaseID == "" || apiToken == "" {
panic("migration: CloudflareD1Client requires accountID, databaseID, apiToken")
}
return &CloudflareD1Client{
httpClient: &http.Client{Timeout: 60 * time.Second},
apiBase: "https://api.cloudflare.com/client/v4",
accountID: accountID,
databaseID: databaseID,
apiToken: apiToken,
}
}
func (c *CloudflareD1Client) SetBaseURL(base string) { c.apiBase = base }
// d1QueryEnvelope matches the D1 query response. Result is an array because
// D1 returns one entry per statement; we only ever send one.
type d1QueryEnvelope struct {
Result []struct {
Results []map[string]any `json:"results"`
Success bool `json:"success"`
} `json:"result"`
Success bool `json:"success"`
Errors []map[string]any `json:"errors"`
}
// Query runs a single SQL statement and returns the row maps in result-set
// order. params is optional; pass nil for parameterless statements.
func (c *CloudflareD1Client) Query(ctx context.Context, sql string, params []any) ([]map[string]any, error) {
payload := map[string]any{"sql": sql}
if len(params) > 0 {
payload["params"] = params
}
body, err := json.Marshal(payload)
if err != nil {
return nil, err
}
endpoint := fmt.Sprintf("%s/accounts/%s/d1/database/%s/query",
c.apiBase, c.accountID, c.databaseID)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.apiToken)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer func() { _ = resp.Body.Close() }()
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("d1 query: status %d: %s", resp.StatusCode, string(raw))
}
var env d1QueryEnvelope
if err := json.Unmarshal(raw, &env); err != nil {
return nil, fmt.Errorf("d1 query: decode: %w", err)
}
if !env.Success {
return nil, fmt.Errorf("d1 query: api error: %v", env.Errors)
}
if len(env.Result) == 0 {
return nil, nil
}
return env.Result[0].Results, nil
}