Files
gh-whamp-whamp-claude-tools…/skills/pocketbase/references/go/go_overview.md
2025-11-30 09:06:02 +08:00

788 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Go Overview - PocketBase
## Overview
PocketBase can be extended using Go, allowing you to:
- Add custom API endpoints
- Implement event hooks
- Create custom database migrations
- Build scheduled jobs
- Add custom middleware
- Integrate external services
- Extend authentication
## Project Structure
```
myapp/
├── go.mod
├── pocketbase.go
├── migrations/
│ └── 1703123456_initial.go
├── hooks/
│ └── hooks.go
└── main.go
```
## Creating a Go Extension
### 1. Initialize Go Module
```bash
go mod init myapp
```
### 2. Install PocketBase SDK
```bash
go get github.com/pocketbase/pocketbase@latest
```
### 3. Basic PocketBase App
Create `main.go`:
```go
package main
import (
"log"
"net/http"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
)
func main() {
app := pocketbase.New()
// Add custom API endpoint
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
// Custom routes
se.Router.GET("/api/hello", func(e *core.RequestEvent) error {
return e.JSON(200, map[string]string{
"message": "Hello from Go!",
})
})
return se.Next()
})
// Start the app
if err := app.Start(); err != nil {
log.Fatal(err)
}
}
```
### 4. Run the Application
```bash
go run main.go pocketbase.go serve --http=0.0.0.0:8090
```
## Core Concepts
### Event Hooks
Execute code on specific events:
```go
// On record create
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordCreateEvent) error {
log.Println("Post created:", e.Record.GetString("title"))
return e.Next()
})
// On record update
app.OnRecordUpdate("posts").BindFunc(func(e *core.RecordUpdateEvent) error {
log.Println("Post updated:", e.Record.GetString("title"))
return e.Next()
})
// On record delete
app.OnRecordDelete("posts").BindFunc(func(e *core.RecordDeleteEvent) error {
log.Println("Post deleted:", e.Record.GetString("title"))
return e.Next()
})
// On authentication
app.OnRecordAuth().BindFunc(func(e *core.RecordAuthEvent) error {
log.Println("User authenticated:", e.Record.GetString("email"))
return e.Next()
})
```
### Event Arguments
PocketBase exposes a different event struct for each hook (see the
[official event hooks reference](https://pocketbase.io/docs/go-event-hooks/)).
Common fields you will interact with include:
- `e.App` the running PocketBase instance (database access, configuration, cron, etc.).
- `e.Record` the record being created, updated, deleted, or authenticated.
- `e.RecordOriginal` the previous value during update hooks.
- `e.Next()` call to continue the handler chain after your logic.
Refer to the linked docs for the complete list of fields exposed by each event type.
## Custom API Endpoints
### Create GET Endpoint
```go
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
se.Router.GET("/api/stats", func(e *core.RequestEvent) error {
totalPosts, err := e.App.CountRecords("posts")
if err != nil {
return e.InternalServerError("failed to count posts", err)
}
totalUsers, err := e.App.CountRecords("users")
if err != nil {
return e.InternalServerError("failed to count users", err)
}
return e.JSON(200, map[string]any{
"total_posts": totalPosts,
"total_users": totalUsers,
"authenticated": e.Auth != nil,
})
})
return se.Next()
})
```
### Create POST Endpoint
```go
import "github.com/pocketbase/dbx"
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
se.Router.POST("/api/search", func(e *core.RequestEvent) error {
// Parse request body
var req struct {
Query string `json:"query"`
}
if err := e.BindBody(&req); err != nil {
return e.BadRequestError("invalid body", err)
}
// Search posts with a safe filter
records, err := e.App.FindRecordsByFilter(
"posts",
"title ~ {:query}",
"-created",
50,
0,
dbx.Params{"query": req.Query},
)
if err != nil {
return e.InternalServerError("Search failed", err)
}
return e.JSON(200, map[string]any{
"results": records,
"count": len(records),
})
})
return se.Next()
})
```
### Custom Middleware
```go
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
se.Router.BindFunc(func(e *core.RequestEvent) error {
// Add CORS headers
e.Response.Header().Set("Access-Control-Allow-Origin", "*")
e.Response.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
e.Response.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if e.Request.Method == http.MethodOptions {
return e.NoContent(http.StatusOK)
}
return e.Next()
})
return se.Next()
})
```
## Database Operations
### Find Records
```go
import "github.com/pocketbase/dbx"
// Find single record
record, err := app.FindRecordById("posts", "RECORD_ID")
// Find multiple records with a filter and pagination
records, err := app.FindRecordsByFilter(
"posts",
"status = {:status}",
"-created",
50,
0,
dbx.Params{"status": "published"},
)
// Custom query with the record query builder
records := []*core.Record{}
err := app.RecordQuery("posts").
AndWhere(dbx.Like("title", "pocketbase")).
OrderBy("created DESC").
Limit(50).
All(&records)
// Find with relations
record, err = app.FindRecordById("posts", "id")
if err == nil {
if errs := app.ExpandRecord(record, []string{"author", "comments"}, nil); len(errs) > 0 {
// handle expand error(s)
}
}
```
### Create Records
```go
collection, err := app.FindCollectionByNameOrId("posts")
if err != nil {
return err
}
record := core.NewRecord(collection)
record.Set("title", "My Post")
record.Set("content", "Post content")
record.Set("author", "USER_ID")
if err := app.Save(record); err != nil {
return err
}
```
### Update Records
```go
record, err := app.FindRecordById("posts", "id")
if err != nil {
return err
}
record.Set("title", "Updated Title")
record.Set("content", "Updated content")
if err := app.Save(record); err != nil {
return err
}
```
### Delete Records
```go
record, err := app.FindRecordById("posts", "id")
if err != nil {
return err
}
if err := app.Delete(record); err != nil {
return err
}
```
### Query Builder
```go
records := []*core.Record{}
err := app.RecordQuery("posts").
AndWhere(dbx.HashExp{"status": "published"}).
AndWhere(dbx.NewExp("created >= {:date}", dbx.Params{"date": "2024-01-01"})).
OrderBy("created DESC").
Offset(0).
Limit(50).
All(&records)
// Combine conditions with OR
records = []*core.Record{}
err = app.RecordQuery("posts").
AndWhere(dbx.Or(
dbx.HashExp{"status": "published"},
dbx.HashExp{"author": userId},
)).
All(&records)
```
## Event Hooks Examples
### Auto-populate Fields
```go
// Auto-set author on post create
app.OnRecordCreateRequest("posts").BindFunc(func(e *core.RecordRequestEvent) error {
if e.Auth != nil {
e.Record.Set("author", e.Auth.Id)
}
return e.Next()
})
// Auto-set slug from title
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordCreateEvent) error {
title := e.Record.GetString("title")
slug := strings.ToLower(strings.ReplaceAll(title, " ", "-"))
e.Record.Set("slug", slug)
return e.Next()
})
```
### Validation
```go
// Custom validation
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordCreateEvent) error {
title := e.Record.GetString("title")
if len(title) < 5 {
return errors.New("title must be at least 5 characters")
}
return e.Next()
})
// Check permissions
app.OnRecordCreateRequest("posts").BindFunc(func(e *core.RecordRequestEvent) error {
if e.Auth == nil {
return e.ForbiddenError("authentication required", nil)
}
role := e.Auth.GetString("role")
if role != "admin" && role != "author" {
return e.ForbiddenError("insufficient permissions", nil)
}
return e.Next()
})
```
### Cascading Updates
```go
// When post is updated, update related comments
app.OnRecordUpdate("posts").BindFunc(func(e *core.RecordUpdateEvent) error {
// Check if status changed
oldStatus := e.RecordOriginal.GetString("status")
newStatus := e.Record.GetString("status")
if oldStatus != newStatus && newStatus == "published" {
comments, err := e.App.FindRecordsByFilter(
"comments",
"post = {:postId}",
"",
0,
0,
dbx.Params{"postId": e.Record.Id},
)
if err != nil {
return err
}
for _, comment := range comments {
comment.Set("status", "approved")
if err := e.App.Save(comment); err != nil {
return err
}
}
}
return e.Next()
})
```
### Send Notifications
```go
// Send email when post is published
app.OnRecordUpdate("posts").BindFunc(func(e *core.RecordUpdateEvent) error {
oldStatus := e.RecordOriginal.GetString("status")
newStatus := e.Record.GetString("status")
if oldStatus != "published" && newStatus == "published" {
author, err := e.App.FindRecordById("users", e.Record.GetString("author"))
if err == nil {
log.Println("Sending notification to:", author.GetString("email"))
}
}
return e.Next()
})
```
### Log Activities
```go
// Log all record changes using the builtin logger
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordCreateEvent) error {
e.App.Logger().Info("post created", "recordId", e.Record.Id)
return e.Next()
})
app.OnRecordUpdate("posts").BindFunc(func(e *core.RecordUpdateEvent) error {
e.App.Logger().Info(
"post updated",
"recordId", e.Record.Id,
"statusFrom", e.RecordOriginal.GetString("status"),
"statusTo", e.Record.GetString("status"),
)
return e.Next()
})
app.OnRecordDelete("posts").BindFunc(func(e *core.RecordDeleteEvent) error {
e.App.Logger().Info("post deleted", "recordId", e.Record.Id)
return e.Next()
})
```
## Scheduled Jobs
### Create Background Job
```go
// Register job
app.Cron().MustAdd("daily-backup", "0 2 * * *", func() {
log.Println("Running daily backup...")
backupDir := "./backups"
if err := os.MkdirAll(backupDir, 0o755); err != nil {
log.Println("Backup failed:", err)
return
}
// Your backup logic here
log.Println("Backup completed")
})
// Or add a job during serve
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
se.App.Cron().MustAdd("cleanup", "@every 5m", func() {
log.Println("Running cleanup task...")
// Cleanup logic
})
return se.Next()
})
```
## File Handling
### Custom File Upload
```go
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
se.Router.POST("/api/upload", func(e *core.RequestEvent) error {
files, err := e.FindUploadedFiles("file")
if err != nil {
return e.BadRequestError("no file uploaded", err)
}
collection, err := e.App.FindCollectionByNameOrId("uploads")
if err != nil {
return e.NotFoundError("uploads collection not found", err)
}
record := core.NewRecord(collection)
// Attach the uploaded file(s) to a file field
record.Set("document", files)
if err := e.App.Save(record, files...); err != nil {
return e.InternalServerError("failed to save file", err)
}
return e.JSON(http.StatusOK, record)
})
return se.Next()
})
```
## Custom Auth Provider
```go
// Custom OAuth provider
app.OnRecordAuthWithOAuth2().BindFunc(func(e *core.RecordAuthWithOAuth2Event) error {
if e.Provider != "custom" {
return e.Next()
}
// Fetch user info from custom provider
userInfo, err := fetchCustomUserInfo(e.OAuth2UserData)
if err != nil {
return err
}
// Find or create user
user, err := e.App.FindAuthRecordByData("users", "email", userInfo.Email)
if err != nil {
collection, err := e.App.FindCollectionByNameOrId("users")
if err != nil {
return err
}
user = core.NewRecord(collection)
user.Set("email", userInfo.Email)
user.Set("password", "") // OAuth users don't need password
user.Set("emailVisibility", false)
user.Set("verified", true)
user.Set("name", userInfo.Name)
if err := e.App.Save(user); err != nil {
return err
}
}
e.Record = user
return e.Next()
})
```
## Testing
### Unit Tests
```go
package main
import (
"testing"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tests"
)
func TestCustomEndpoint(t *testing.T) {
app := pocketbase.NewWithConfig(config{})
// Add test endpoint
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
se.Router.GET("/api/test", func(e *core.RequestEvent) error {
return e.JSON(200, map[string]string{
"status": "ok",
})
})
return se.Next()
})
e := tests.NewRequestEvent(app, nil)
// Test endpoint
e.GET("/api/test").Expect(t).Status(200).JSON().Equal(map[string]interface{}{
"status": "ok",
})
}
```
### Integration Tests
```go
func TestRecordCreation(t *testing.T) {
app := pocketbase.New()
app.MustSeed()
client := tests.NewClient(app)
// Test authenticated request
auth := client.AuthRecord("users", "test@example.com", "password")
post := client.CreateRecord("posts", map[string]interface{}{
"title": "Test Post",
"content": "Test content",
}, auth.Token)
if post.GetString("title") != "Test Post" {
t.Errorf("Expected title 'Test Post', got %s", post.GetString("title"))
}
}
```
## Deployment
### Build and Run
```bash
# Build
go build -o myapp main.go
# Run
./myapp serve --http=0.0.0.0:8090
```
### Docker Deployment
```dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN go build -o myapp main.go
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/myapp .
COPY --from=builder /app/pocketbase ./
CMD ["./myapp", "serve", "--http=0.0.0.0:8090"]
```
## Best Practices
### 1. Error Handling
```go
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordCreateEvent) error {
if err := validatePost(e.Record); err != nil {
return err
}
return e.Next()
})
func validatePost(record *core.Record) error {
title := record.GetString("title")
if len(title) == 0 {
return errors.New("title is required")
}
if len(title) > 200 {
return errors.New("title too long")
}
return nil
}
```
### 2. Logging
```go
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordCreateEvent) error {
e.App.Logger().Info("Post created",
"id", e.Record.Id,
"title", e.Record.GetString("title"),
)
return e.Next()
})
```
### 3. Security
```go
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
limiter := rate.NewLimiter(10, 20) // 10 req/sec, burst 20
se.Router.BindFunc(func(e *core.RequestEvent) error {
if !limiter.Allow() {
return e.TooManyRequestsError("rate limit exceeded", nil)
}
return e.Next()
})
return se.Next()
})
```
### 4. Configuration
```go
type Config struct {
ExternalAPIKey string
EmailFrom string
}
func (c Config) Name() string {
return "myapp"
}
func main() {
app := pocketbase.NewWithConfig(Config{
ExternalAPIKey: os.Getenv("API_KEY"),
EmailFrom: "noreply@example.com",
})
// Use config in hooks
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordCreateEvent) error {
cfg := e.App.Config().(*Config)
// Use cfg.ExternalAPIKey
return e.Next()
})
}
```
## Common Patterns
### 1. Soft Delete
```go
app.OnRecordDelete("posts").BindFunc(func(e *core.RecordDeleteEvent) error {
// Instead of deleting, mark as deleted
e.Record.Set("status", "deleted")
e.Record.Set("deleted_at", time.Now())
if err := e.App.Save(e.Record); err != nil {
return err
}
return e.Next()
})
```
### 2. Audit Trail
```go
app.OnRecordCreateRequest("").BindFunc(func(e *core.RecordRequestEvent) error {
if e.Collection.Name == "posts" || e.Collection.Name == "comments" {
if e.Auth != nil {
e.Record.Set("created_by", e.Auth.Id)
}
if info, err := e.RequestInfo(); err == nil {
e.Record.Set("created_ip", info.RealIP)
}
}
return e.Next()
})
app.OnRecordUpdateRequest("").BindFunc(func(e *core.RecordRequestEvent) error {
if e.Collection.Name == "posts" || e.Collection.Name == "comments" {
if e.Auth != nil {
e.Record.Set("updated_by", e.Auth.Id)
}
e.Record.Set("updated_at", time.Now())
}
return e.Next()
})
```
### 3. Data Synchronization
```go
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordCreateEvent) error {
// Sync with external service
if err := syncToExternalAPI(e.Record); err != nil {
e.App.Logger().Warn("sync failed", "error", err)
}
return e.Next()
})
func syncToExternalAPI(record *core.Record) error {
// Implement external API sync
return nil
}
```
## Related Topics
- [Event Hooks](go_event_hooks.md) - Detailed hook documentation
- [Database](go_database.md) - Database operations
- [Routing](go_routing.md) - Custom API endpoints
- [Migrations](go_migrations.md) - Database migrations
- [Testing](go_testing.md) - Testing strategies
- [Logging](go_logging.md) - Logging and monitoring