PocketBase view collection benutzen. Dokumentation aktualisieren.
This commit is contained in:
@@ -14,10 +14,7 @@ import (
|
||||
|
||||
// Config holds the compile-time settings for the application.
|
||||
type Config struct {
|
||||
PBURL string
|
||||
PBUser string
|
||||
PBPassword string
|
||||
PBCategory string
|
||||
PBURL string
|
||||
}
|
||||
|
||||
// App orchestrates the full pipeline.
|
||||
@@ -31,7 +28,7 @@ type App struct {
|
||||
// New creates a new App with the given configuration.
|
||||
func New(cfg Config) *App {
|
||||
return &App{
|
||||
pocketbase: pocketbase.New(cfg.PBURL, cfg.PBUser, cfg.PBPassword, cfg.PBCategory),
|
||||
pocketbase: pocketbase.New(cfg.PBURL),
|
||||
scraper: royalroad.NewScraper(),
|
||||
outW: os.Stdout,
|
||||
errW: os.Stderr,
|
||||
|
||||
@@ -1,50 +1,37 @@
|
||||
package pocketbase
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client handles interactions with the PocketBase API.
|
||||
type Client struct {
|
||||
baseURL string
|
||||
user string
|
||||
password string
|
||||
category string
|
||||
endpoint *url.URL
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
// New creates a new PocketBase client.
|
||||
func New(baseURL, user, password, category string) *Client {
|
||||
// Normalise: ensure baseURL does not already end with /api.
|
||||
baseURL = strings.TrimSuffix(baseURL, "/api")
|
||||
baseURL = strings.TrimSuffix(baseURL, "/")
|
||||
func New(endpoint string) *Client {
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil || u == nil {
|
||||
return &Client{
|
||||
endpoint: &url.URL{},
|
||||
http: &http.Client{Timeout: 30 * time.Second},
|
||||
}
|
||||
}
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
user: user,
|
||||
password: password,
|
||||
category: category,
|
||||
endpoint: u,
|
||||
http: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type authRequest struct {
|
||||
Identity string `json:"identity"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type authResponse struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type record struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
@@ -53,64 +40,11 @@ type recordsResponse struct {
|
||||
Items []record `json:"items"`
|
||||
}
|
||||
|
||||
// GetBookmarkURLs authenticates and retrieves URLs from the bookmarks collection.
|
||||
// GetBookmarkURLs retrieves URLs from the collection.
|
||||
func (c *Client) GetBookmarkURLs(ctx context.Context) ([]string, error) {
|
||||
token, err := c.authenticate(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pocketbase auth: %w", err)
|
||||
}
|
||||
|
||||
urls, err := c.fetchURLs(ctx, token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pocketbase fetch URLs: %w", err)
|
||||
}
|
||||
|
||||
return urls, nil
|
||||
}
|
||||
|
||||
func (c *Client) authenticate(ctx context.Context) (string, error) {
|
||||
reqBody, err := json.Marshal(authRequest{
|
||||
Identity: c.user,
|
||||
Password: c.password,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
authURL := c.baseURL + "/api/collections/users/auth-with-password"
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", authURL, bytes.NewBuffer(reqBody))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("unexpected status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var authResp authResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return authResp.Token, nil
|
||||
}
|
||||
|
||||
func (c *Client) fetchURLs(ctx context.Context, token string) ([]string, error) {
|
||||
// Filter: category=<value>, perPage=200, fields=url
|
||||
filter := fmt.Sprintf(`category="%s"`, c.category)
|
||||
u, err := url.Parse(c.baseURL + "/api/collections/bookmarks/records")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid base URL: %w", err)
|
||||
}
|
||||
// Clone the endpoint URL to avoid mutation on repeated calls.
|
||||
u := *c.endpoint
|
||||
q := u.Query()
|
||||
q.Set("filter", filter)
|
||||
q.Set("perPage", "200")
|
||||
q.Set("fields", "url")
|
||||
u.RawQuery = q.Encode()
|
||||
@@ -119,7 +53,6 @@ func (c *Client) fetchURLs(ctx context.Context, token string) ([]string, error)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", token)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,29 +10,28 @@ import (
|
||||
|
||||
func TestGetBookmarkURLs_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/api/collections/users/auth-with-password" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(authResponse{Token: "test-token"})
|
||||
return
|
||||
// No auth needed - just return records.
|
||||
if r.URL.Path != "/" {
|
||||
t.Errorf("Expected path '/', got '%s'", r.URL.Path)
|
||||
}
|
||||
if r.URL.Path == "/api/collections/bookmarks/records" {
|
||||
if r.Header.Get("Authorization") != "test-token" {
|
||||
t.Errorf("Expected Authorization header 'test-token', got '%s'", r.Header.Get("Authorization"))
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(recordsResponse{
|
||||
Items: []record{
|
||||
{URL: "https://royalroad.com/fiction/1"},
|
||||
{URL: "https://royalroad.com/fiction/2"},
|
||||
},
|
||||
})
|
||||
return
|
||||
// Verify query params.
|
||||
if r.URL.Query().Get("perPage") != "200" {
|
||||
t.Errorf("Expected perPage=200, got '%s'", r.URL.Query().Get("perPage"))
|
||||
}
|
||||
t.Errorf("Unexpected request to %s", r.URL.Path)
|
||||
if r.URL.Query().Get("fields") != "url" {
|
||||
t.Errorf("Expected fields=url, got '%s'", r.URL.Query().Get("fields"))
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(recordsResponse{
|
||||
Items: []record{
|
||||
{URL: "https://royalroad.com/fiction/1"},
|
||||
{URL: "https://royalroad.com/fiction/2"},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New(server.URL, "user", "pass", "Geschichten")
|
||||
client := New(server.URL)
|
||||
urls, err := client.GetBookmarkURLs(context.Background())
|
||||
|
||||
if err != nil {
|
||||
@@ -46,20 +45,16 @@ func TestGetBookmarkURLs_Success(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBookmarkURLs_AuthFailure(t *testing.T) {
|
||||
func TestGetBookmarkURLs_Non200(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/api/collections/users/auth-with-password" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
t.Errorf("Unexpected request to %s", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New(server.URL, "user", "pass", "Geschichten")
|
||||
client := New(server.URL)
|
||||
_, err := client.GetBookmarkURLs(context.Background())
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected error on auth failure, got nil")
|
||||
t.Error("Expected error on non-200 status, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user