feat: simplify SOP to immediate-effect configuration
This commit is contained in:
137
internal/multitable/client.go
Normal file
137
internal/multitable/client.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package multitable
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
apiKey string
|
||||
http *http.Client
|
||||
mu sync.Mutex
|
||||
sourceColumns map[uint64]string
|
||||
}
|
||||
|
||||
func NewClient(baseURL, apiKey string, timeout time.Duration) *Client {
|
||||
return &Client{baseURL: strings.TrimRight(baseURL, "/"), apiKey: apiKey, http: &http.Client{Timeout: timeout}, sourceColumns: make(map[uint64]string)}
|
||||
}
|
||||
|
||||
func (c *Client) Upsert(ctx context.Context, tableID uint64, sourceID string, data map[string]interface{}) error {
|
||||
sourceColumn, err := c.sourceColumn(ctx, tableID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filters, err := json.Marshal([]map[string]interface{}{{"col": sourceColumn, "op": "eq", "val": sourceID}})
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode filters: %w", err)
|
||||
}
|
||||
endpoint := c.endpoint("tables", fmt.Sprint(tableID), "records")
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse records endpoint: %w", err)
|
||||
}
|
||||
query := u.Query()
|
||||
query.Set("page", "1")
|
||||
query.Set("pageSize", "2")
|
||||
query.Set("filters", string(filters))
|
||||
u.RawQuery = query.Encode()
|
||||
|
||||
var listed struct {
|
||||
Items []struct {
|
||||
ID uint64 `json:"id"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := c.doJSON(ctx, http.MethodGet, u.String(), nil, &listed); err != nil {
|
||||
return err
|
||||
}
|
||||
body := map[string]interface{}{"data": data}
|
||||
if len(listed.Items) == 0 {
|
||||
return c.doJSON(ctx, http.MethodPost, endpoint, body, nil)
|
||||
}
|
||||
return c.doJSON(ctx, http.MethodPut, c.endpoint("tables", fmt.Sprint(tableID), "records", fmt.Sprint(listed.Items[0].ID)), body, nil)
|
||||
}
|
||||
|
||||
func (c *Client) sourceColumn(ctx context.Context, tableID uint64) (string, error) {
|
||||
c.mu.Lock()
|
||||
if columnID, ok := c.sourceColumns[tableID]; ok {
|
||||
c.mu.Unlock()
|
||||
return columnID, nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
var columns []struct {
|
||||
ID uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := c.doJSON(ctx, http.MethodGet, c.endpoint("tables", fmt.Sprint(tableID), "columns"), nil, &columns); err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, column := range columns {
|
||||
if column.Name == "来源ID" {
|
||||
columnID := fmt.Sprint(column.ID)
|
||||
c.mu.Lock()
|
||||
c.sourceColumns[tableID] = columnID
|
||||
c.mu.Unlock()
|
||||
return columnID, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("multitable table %d has no 来源ID column", tableID)
|
||||
}
|
||||
|
||||
func (c *Client) endpoint(parts ...string) string {
|
||||
return c.baseURL + "/" + path.Join(parts...)
|
||||
}
|
||||
|
||||
func (c *Client) doJSON(ctx context.Context, method, endpoint string, input interface{}, output interface{}) error {
|
||||
var body io.Reader
|
||||
if input != nil {
|
||||
encoded, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode request: %w", err)
|
||||
}
|
||||
body = bytes.NewReader(encoded)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, endpoint, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
req.Header.Set("X-API-Key", c.apiKey)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if input != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("call multitable API: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
|
||||
if err != nil {
|
||||
return fmt.Errorf("read multitable response: %w", err)
|
||||
}
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return fmt.Errorf("multitable API returned %d: %s", resp.StatusCode, truncate(string(data), 512))
|
||||
}
|
||||
if output != nil && len(data) > 0 {
|
||||
if err := json.Unmarshal(data, output); err != nil {
|
||||
return fmt.Errorf("decode multitable response: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func truncate(value string, max int) string {
|
||||
if len(value) <= max {
|
||||
return value
|
||||
}
|
||||
return value[:max]
|
||||
}
|
||||
Reference in New Issue
Block a user