package main import ( "net/http" "github.com/pocketbase/pocketbase/core" ) // setupAPIRoutes sets up the event store API routes func setupAPIRoutes(app core.App, eventStore *SimpleEventStore) { app.OnServe().BindFunc(func(se *core.ServeEvent) error { // JSON Patch endpoint using PATCH method - ONE OPERATION PER REQUEST se.Router.PATCH("/api/collections/{collection}/items/{itemId}", func(e *core.RequestEvent) error { collection := e.Request.PathValue("collection") itemID := e.Request.PathValue("itemId") if collection == "" || itemID == "" { return e.BadRequestError("Collection and itemId are required", nil) } var operation struct { Op string `json:"op"` Path string `json:"path"` Value string `json:"value"` From string `json:"from"` } if err := e.BindBody(&operation); err != nil { return e.BadRequestError("Failed to parse operation data", err) } // Create event with single operation incomingEvent := &Event{ ItemID: itemID, Collection: collection, Operation: operation.Op, Path: operation.Path, Value: operation.Value, From: operation.From, } // Process the event processedEvent, err := eventStore.ProcessEvent(incomingEvent) if err != nil { return e.InternalServerError("Failed to process event", err) } return e.JSON(http.StatusOK, processedEvent) }) // Legacy POST endpoint for compatibility se.Router.POST("/api/events", func(e *core.RequestEvent) error { var incomingEvent Event if err := e.BindBody(&incomingEvent); err != nil { return e.BadRequestError("Failed to parse event data", err) } // Validate required fields if incomingEvent.ItemID == "" || incomingEvent.Collection == "" || incomingEvent.Operation == "" { return e.BadRequestError("Missing required fields: item_id, collection, operation", nil) } // Process the event processedEvent, err := eventStore.ProcessEvent(&incomingEvent) if err != nil { return e.InternalServerError("Failed to process event", err) } return e.JSON(http.StatusCreated, processedEvent) }) // Sync endpoint for clients se.Router.POST("/api/sync", func(e *core.RequestEvent) error { var syncReq SyncRequest if err := e.BindBody(&syncReq); err != nil { return e.BadRequestError("Failed to parse sync request", err) } // Check if client is in sync isValid, err := eventStore.ValidateSync(syncReq.LastSeq, syncReq.LastHash) if err != nil { return e.InternalServerError("Failed to validate sync", err) } var response SyncResponse if !isValid { // Full sync needed - send all events events, err := eventStore.GetEventsSince(0) if err != nil { return e.InternalServerError("Failed to get events", err) } response.Events = events response.FullSync = true } else { // Incremental sync - send events since last sequence events, err := eventStore.GetEventsSince(syncReq.LastSeq) if err != nil { return e.InternalServerError("Failed to get events", err) } response.Events = events response.FullSync = false } // Get current state latestEvent, err := eventStore.GetLatestEvent() if err != nil { return e.InternalServerError("Failed to get latest event", err) } if latestEvent != nil { response.CurrentSeq = latestEvent.Seq response.CurrentHash = latestEvent.Hash } return e.JSON(http.StatusOK, response) }) // Get all items endpoint se.Router.GET("/api/items/{collection}", func(e *core.RequestEvent) error { collection := e.Request.PathValue("collection") if collection == "" { return e.BadRequestError("Collection name required", nil) } items, err := eventStore.GetAllItems(collection) if err != nil { return e.InternalServerError("Failed to get items", err) } return e.JSON(http.StatusOK, map[string]interface{}{ "items": items, }) }) // Batch events endpoint for client to send multiple events se.Router.POST("/api/events/batch", func(e *core.RequestEvent) error { var events []Event if err := e.BindBody(&events); err != nil { return e.BadRequestError("Failed to parse events data", err) } processedEvents := make([]Event, 0, len(events)) for _, incomingEvent := range events { // Validate required fields if incomingEvent.ItemID == "" || incomingEvent.Collection == "" || incomingEvent.Operation == "" { return e.BadRequestError("Missing required fields in event: item_id, collection, operation", nil) } processedEvent, err := eventStore.ProcessEvent(&incomingEvent) if err != nil { return e.InternalServerError("Failed to process event", err) } processedEvents = append(processedEvents, *processedEvent) } return e.JSON(http.StatusCreated, map[string]interface{}{ "events": processedEvents, }) }) // Get current state endpoint se.Router.GET("/api/state", func(e *core.RequestEvent) error { latestEvent, err := eventStore.GetLatestEvent() if err != nil { return e.InternalServerError("Failed to get latest event", err) } response := map[string]interface{}{ "seq": 0, "hash": "", } if latestEvent != nil { response["seq"] = latestEvent.Seq response["hash"] = latestEvent.Hash } return e.JSON(http.StatusOK, response) }) return se.Next() }) }