mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
One path: /v1/*. We do not use /api/ — that's Casdoor's path. The
BASE_API_PREFIX env knob is gone; the prefix is hard-coded in
apis/base.go and apis/serve.go banner.
Server-side mounts:
- apis: bindHealthApi, bindCollectionApi, bindRecordCrudApi, etc. all
attach under r.Group("/v1") (was r.Group(BASE_API_PREFIX, default /api))
- platform plugin: /api/platform/* -> /v1/platform/* (including auth and
org sub-groups)
- kms plugin: /api/kms/* -> /v1/kms/*
- functions plugin: /api/functions/* -> /v1/functions/*
- cloudsql plugin: /api/cloud-sql/*, /api/meta/* -> /v1/*
- superuser OIDC: /_/api/oidc/config -> /_/v1/oidc/config
- core settings: default RateLimitRule labels /api/batch, /api/ ->
/v1/batch, /v1/
- apis/batch.go: ValidBatchActions regexes match /v1/collections/*;
internal rewrite targets /v1/collections/* (was /v1/base/collections/*,
a stale artifact)
Clients:
- sdk/base-js (BaseClient.collections, files, realtime, CRDT WS,
signIn/refresh/health) targets /v1/*
- sdk/base-react (use-crdt WS path) targets /v1/crdt
- ui-react admin (lib/api.ts, settings.application copy) targets /v1/*
- cmd/cli (NewClient.APIPrefix) defaults to /v1; client_test.go and
cmd/cli_test.go stubServer use /v1/*
- cmd/typegen calls /v1/collections
Upstream paths untouched: Casdoor IAM (/api/login, /api/userinfo,
/api/signup, /api/send-verification-code, /api/get-user), KMS Infisical
(/api/v1/auth/universal-auth/login, /api/v2/workspace/environments),
Kubernetes tasks (/api/v1/namespaces/...), IPFS (/api/v0/...), Gitea/
GitLab/Discord/Strava/Patreon/Box/Wakatime upstream APIs. Those are
their contracts, not ours.
Tests: all 437 prior /api/ test URLs across apis/*_test.go and
plugins/platform/auth_proxy_test.go updated to /v1/. Build + go vet
clean. ./apis ./core ./plugins/platform ./plugins/jsvm ./forms all
green. Pre-existing red probes (network/TestRedProbe_*,
migratecmd/TestAutomigrateCollection*) unchanged.
125 lines
2.9 KiB
Go
125 lines
2.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Client is a thin HTTP client for the Base API.
|
|
type Client struct {
|
|
BaseURL string
|
|
Token string
|
|
Tenant string
|
|
APIPrefix string
|
|
HTTP *http.Client
|
|
}
|
|
|
|
// NewClient returns a Client with sensible defaults.
|
|
func NewClient(baseURL, token, tenant string) *Client {
|
|
return &Client{
|
|
BaseURL: strings.TrimRight(baseURL, "/"),
|
|
Token: token,
|
|
Tenant: tenant,
|
|
APIPrefix: "/v1",
|
|
HTTP: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
}
|
|
|
|
// Do executes a raw HTTP request against the Base API and returns
|
|
// the decoded JSON body. Non-2xx responses are returned as an error.
|
|
func (c *Client) Do(method, path string, body any) (json.RawMessage, int, error) {
|
|
var reqBody io.Reader
|
|
if body != nil {
|
|
b, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("marshal request body: %w", err)
|
|
}
|
|
reqBody = bytes.NewReader(b)
|
|
}
|
|
|
|
fullURL := c.BaseURL + c.APIPrefix + path
|
|
|
|
req, err := http.NewRequest(method, fullURL, reqBody)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("build request: %w", err)
|
|
}
|
|
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
if c.Token != "" {
|
|
req.Header.Set("Authorization", c.Token)
|
|
}
|
|
if c.Tenant != "" {
|
|
req.Header.Set("X-Org-Id", c.Tenant)
|
|
}
|
|
|
|
resp, err := c.HTTP.Do(req)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("http request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
raw, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, resp.StatusCode, fmt.Errorf("read response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return nil, resp.StatusCode, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(raw))
|
|
}
|
|
|
|
if len(raw) == 0 {
|
|
return nil, resp.StatusCode, nil
|
|
}
|
|
|
|
return json.RawMessage(raw), resp.StatusCode, nil
|
|
}
|
|
|
|
// Get performs a GET request.
|
|
func (c *Client) Get(path string) (json.RawMessage, int, error) {
|
|
return c.Do(http.MethodGet, path, nil)
|
|
}
|
|
|
|
// Post performs a POST request with a JSON body.
|
|
func (c *Client) Post(path string, body any) (json.RawMessage, int, error) {
|
|
return c.Do(http.MethodPost, path, body)
|
|
}
|
|
|
|
// Patch performs a PATCH request with a JSON body.
|
|
func (c *Client) Patch(path string, body any) (json.RawMessage, int, error) {
|
|
return c.Do(http.MethodPatch, path, body)
|
|
}
|
|
|
|
// Delete performs a DELETE request.
|
|
func (c *Client) Delete(path string) (json.RawMessage, int, error) {
|
|
return c.Do(http.MethodDelete, path, nil)
|
|
}
|
|
|
|
// BuildQuery encodes query parameters for list endpoints.
|
|
func BuildQuery(filter string, limit int, sort string, extra map[string]string) string {
|
|
params := url.Values{}
|
|
if filter != "" {
|
|
params.Set("filter", filter)
|
|
}
|
|
if limit > 0 {
|
|
params.Set("perPage", fmt.Sprintf("%d", limit))
|
|
}
|
|
if sort != "" {
|
|
params.Set("sort", sort)
|
|
}
|
|
for k, v := range extra {
|
|
params.Set(k, v)
|
|
}
|
|
if len(params) == 0 {
|
|
return ""
|
|
}
|
|
return "?" + params.Encode()
|
|
}
|