Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:06:02 +08:00
commit 02cab85880
53 changed files with 12367 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
# go_collections - Go Extensions
## Overview
This file covers go_collections in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,85 @@
# go_console_commands - Go Extensions
## Overview
PocketBase exposes Cobra-based console commands that you can extend from either Go extensions or JavaScript `pb_hooks`. Use them for background jobs, data migrations, administrative helpers, or build-time automation. Review [`go_overview.md`](go_overview.md) for extension setup and wiring custom commands into the root CLI.
---
## Data Migration Commands
Import/export workflows benefit from running inside PocketBase where you can wrap operations in transactions, reuse the ORM, and uphold access rules. At a minimum:
1. Register a command on `$app.RootCmd()` (Go) or `$app.rootCmd` (JS hooks).
2. Accept flags for batch size, dry-run execution, and optional upsert keys.
3. Wrap writes in `app.RunInTransaction(...)` when consistency matters.
4. Use `app.FindCollectionByNameOrId` and `core.NewRecord` (Go) or `new Record(collection)` (JS) to construct records.
5. Stream large payloads in chunks to keep memory stable and close files promptly.
### Go skeleton
```go
package migrations
import (
"encoding/json"
"os"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/spf13/cobra"
)
func Register(app *pocketbase.PocketBase) {
var batchSize int
cmd := &cobra.Command{
Use: "data:import <collection> <file>",
Short: "Import records from a JSON file",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
collectionName, filePath := args[0], args[1]
payload, err := os.ReadFile(filePath)
if err != nil {
return err
}
var rows []map[string]any
if err := json.Unmarshal(payload, &rows); err != nil {
return err
}
return app.RunInTransaction(func(txApp core.App) error {
collection, err := txApp.FindCollectionByNameOrId(collectionName)
if err != nil {
return err
}
for idx, row := range rows {
record := core.NewRecord(collection)
record.Load(row)
if err := txApp.Save(record); err != nil {
return err
}
if batchSize > 0 && (idx+1)%batchSize == 0 {
cmd.Printf("Imported %d records\n", idx+1)
}
}
return nil
})
},
}
cmd.Flags().IntVar(&batchSize, "batch", 500, "Records per transaction chunk")
app.RootCmd().AddCommand(cmd)
}
```
Adapt the skeleton with streaming readers, upsert logic, or batch logging as needed. For a JavaScript equivalent, see the example in [Data Migration Workflows](../core/data_migration.md#option-2-custom-cli-commands).
---
## Automation Notes
- Prefer purpose-built commands (`data:import`, `data:export`) to avoid confusion with the built-in `migrate` schema command.
- Add validation flags (e.g., `--dry-run`, `--allow-save-no-validate`) to make commands safer in production.
- Consider registering complementary commands for exporting manifests, truncating collections, or rehydrating relation fields.
- If you need to expose the same logic via HTTP, see [`go_routing.md`](go_routing.md) for admin-only endpoints that invoke the same import/export routines under the hood.

View File

@@ -0,0 +1,130 @@
# Database Operations - Go Extensions
## Overview
PocketBase provides a powerful database API for Go extensions, allowing you to perform complex queries, transactions, and data operations.
## Basic Queries
### Find Records
```go
import "github.com/pocketbase/dbx"
// Find single record
record, err := app.FindRecordById("posts", "RECORD_ID")
// Find multiple records with filter
records, err := app.FindRecordsByFilter(
"posts",
"status = {:status}",
"-created",
50,
0,
dbx.Params{"status": "published"},
)
// Fetch all matching records without pagination
records, err = app.FindRecordsByFilter(
"posts",
"status = {:status}",
"-created",
0,
0,
dbx.Params{"status": "published"},
)
```
### Query Builder
```go
import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
)
records := []*core.Record{}
err := app.RecordQuery("posts").
AndWhere(dbx.HashExp{"status": "published"}).
AndWhere(dbx.NewExp("created >= {:date}", dbx.Params{"date": "2024-01-01"})).
AndWhere(dbx.Or(
dbx.HashExp{"author": userId},
dbx.HashExp{"featured": true},
)).
OrderBy("created DESC").
Offset(0).
Limit(50).
All(&records)
```
### Transactions
```go
import "github.com/pocketbase/pocketbase/core"
err := app.RunInTransaction(func(txApp core.App) error {
collection, err := txApp.FindCollectionByNameOrId("posts")
if err != nil {
return err
}
post := core.NewRecord(collection)
post.Set("title", "New Post")
if err := txApp.Save(post); err != nil {
return err
}
commentsCol, err := txApp.FindCollectionByNameOrId("comments")
if err != nil {
return err
}
comment := core.NewRecord(commentsCol)
comment.Set("post", post.Id)
comment.Set("content", "First comment")
return txApp.Save(comment)
})
```
### Bulk import pattern
```go
import (
"encoding/json"
"os"
)
func importFile(app core.App, collectionName, path string) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
var rows []map[string]any
if err := json.Unmarshal(data, &rows); err != nil {
return err
}
return app.RunInTransaction(func(tx core.App) error {
col, err := tx.FindCollectionByNameOrId(collectionName)
if err != nil {
return err
}
for _, row := range rows {
rec := core.NewRecord(col)
rec.Load(row)
if err := tx.Save(rec); err != nil {
return err
}
}
return nil
})
}
```
- Split large imports into chunks to keep memory usage predictable.
- Prefer `RunInTransaction` for atomicity; if you intentionally bypass validation use `SaveNoValidate` after cleaning the data.
- Coordinate the schema setup with migrations—see [`go_migrations.md`](go_migrations.md) and [Data Migration Workflows](../core/data_migration.md).
---
**Note:** See [go_overview.md](go_overview.md) and the [official database guide](https://pocketbase.io/docs/go-database/) for comprehensive coverage.

View File

@@ -0,0 +1,104 @@
# Event Hooks - Go Extensions
## Overview
Event hooks allow you to execute custom logic when specific events occur in PocketBase, such as record creation, updates, authentication, or API requests.
## Hook Types
### Record Hooks
- `OnRecordCreate()` - Before/after record creation
- `OnRecordUpdate()` - Before/after record updates
- `OnRecordDelete()` - Before/after record deletion
- `OnRecordList()` - Before/after listing records
- `OnRecordView()` - Before/after viewing a record
### Auth Hooks
- `OnRecordAuth()` - After authentication
- `OnRecordAuthWithPassword()` - Password authentication
- `OnRecordAuthWithOAuth2()` - OAuth2 authentication
- `OnRecordRequestPasswordReset()` - Password reset request
- `OnRecordConfirmPasswordReset()` - Password reset confirmation
### Serve Hooks
- `OnServe()` - Customize the HTTP server before it starts serving requests
- `OnTerminate()` - Handle graceful shutdown logic
## Examples
### Auto-populate Fields
```go
app.OnRecordCreateRequest("posts").BindFunc(func(e *core.RecordRequestEvent) error {
if e.Auth != nil {
e.Record.Set("author", e.Auth.Id)
}
title := e.Record.GetString("title")
slug := strings.ToLower(strings.ReplaceAll(title, " ", "-"))
e.Record.Set("slug", slug)
return e.Next()
})
```
### Validation
```go
import "github.com/pocketbase/dbx"
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")
}
if _, err := e.App.FindFirstRecordByFilter(
"posts",
"title = {:title}",
dbx.Params{"title": title},
); err == nil {
return errors.New("title already exists")
}
return e.Next()
})
```
### Cascading Updates
```go
import "github.com/pocketbase/dbx"
app.OnRecordUpdate("posts").BindFunc(func(e *core.RecordUpdateEvent) error {
oldStatus := e.RecordOriginal.GetString("status")
newStatus := e.Record.GetString("status")
if oldStatus != "published" && 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()
})
```
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for detailed hook documentation.

View File

@@ -0,0 +1,9 @@
# go_filesystem - Go Extensions
## Overview
This file covers go_filesystem in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,9 @@
# go_jobs_scheduling - Go Extensions
## Overview
This file covers go_jobs_scheduling in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,9 @@
# go_logging - Go Extensions
## Overview
This file covers go_logging in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,16 @@
# go_migrations - Go Extensions
## Overview
PocketBase migrations capture schema changes and ensure collections look identical across environments. Generate them with `./pocketbase migrate collections`, edit them manually, or author Go-based migrations for more complex logic. See [go_overview.md](go_overview.md) for a complete walkthrough.
---
## Preparing for Data Imports
1. **Define collections and fields before importing data.** Either create them in the Admin UI and snapshot with `migrate collections`, or programmatically add them inside a migration using `app.FindCollectionByNameOrId` and the `schema` helpers.
2. **Record unique constraints in migrations.** Use indexes and validation rules so the import scripts can rely on deterministic upsert keys.
3. **Version relation changes.** If you add or rename relation fields, ship the migration alongside your import plan; run it first so the import sees the correct schema.
4. **Keep migrations idempotent.** Wrap schema mutations in guards (`if collection == nil { ... }`) when writing Go migrations to avoid panics on re-run.
For end-to-end import/export workflows, continue with [Data Migration Workflows](../core/data_migration.md).

View File

@@ -0,0 +1,9 @@
# go_miscellaneous - Go Extensions
## Overview
This file covers go_miscellaneous in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,787 @@
# 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

View File

@@ -0,0 +1,9 @@
# go_realtime - Go Extensions
## Overview
This file covers go_realtime in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,9 @@
# go_record_proxy - Go Extensions
## Overview
This file covers go_record_proxy in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,9 @@
# go_records - Go Extensions
## Overview
This file covers go_records in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,9 @@
# go_rendering_templates - Go Extensions
## Overview
This file covers go_rendering_templates in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,61 @@
# Routing - Go Extensions
## Overview
Create custom API endpoints and middleware using PocketBase's routing system.
## Custom Endpoints
```go
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
// GET endpoint
se.Router.GET("/api/custom", func(e *core.RequestEvent) error {
return e.JSON(200, map[string]string{"status": "ok"})
})
// POST endpoint
se.Router.POST("/api/custom", func(e *core.RequestEvent) error {
return e.JSON(200, map[string]string{"message": "created"})
})
return se.Next()
})
```
### Admin-only import/export endpoints
Expose migration jobs over HTTP when you need a web dashboard trigger:
```go
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
se.Router.POST("/api/admin/data/import", func(e *core.RequestEvent) error {
if !apis.IsSuperUser(e.Auth) {
return apis.NewForbiddenError("admin token required", nil)
}
var payload struct {
Collection string `json:"collection"`
File string `json:"file"`
DryRun bool `json:"dryRun"`
}
if err := e.BindBody(&payload); err != nil {
return err
}
return e.App.RunInTransaction(func(txApp core.App) error {
// invoke shared import logic here
return nil
})
})
return se.Next()
})
```
- Require superuser tokens (or tighter auth) before touching data.
- For long-running operations, enqueue a job and return an ID the client can poll.
- Keep HTTP handlers thin—delegate to the same helpers used by the CLI commands described in [Data Migration Workflows](../core/data_migration.md).
---
**Note:** See [go_overview.md](go_overview.md) for detailed routing documentation.

View File

@@ -0,0 +1,9 @@
# go_sdk - Go Extensions
## Overview
This file covers go_sdk in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,9 @@
# go_sending_emails - Go Extensions
## Overview
This file covers go_sending_emails in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,9 @@
# go_testing - Go Extensions
## Overview
This file covers go_testing in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.