Deretardify the fucking events
Good job claude complicate everything why don't you
This commit is contained in:
30
api.go
30
api.go
@@ -9,7 +9,7 @@ import (
|
|||||||
// setupAPIRoutes sets up the event store API routes
|
// setupAPIRoutes sets up the event store API routes
|
||||||
func setupAPIRoutes(app core.App, eventStore *SimpleEventStore) {
|
func setupAPIRoutes(app core.App, eventStore *SimpleEventStore) {
|
||||||
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
||||||
// JSON Patch endpoint using PATCH method
|
// JSON Patch endpoint using PATCH method - ONE OPERATION PER REQUEST
|
||||||
se.Router.PATCH("/api/collections/{collection}/items/{itemId}", func(e *core.RequestEvent) error {
|
se.Router.PATCH("/api/collections/{collection}/items/{itemId}", func(e *core.RequestEvent) error {
|
||||||
collection := e.Request.PathValue("collection")
|
collection := e.Request.PathValue("collection")
|
||||||
itemID := e.Request.PathValue("itemId")
|
itemID := e.Request.PathValue("itemId")
|
||||||
@@ -18,16 +18,24 @@ func setupAPIRoutes(app core.App, eventStore *SimpleEventStore) {
|
|||||||
return e.BadRequestError("Collection and itemId are required", nil)
|
return e.BadRequestError("Collection and itemId are required", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var patches []PatchOperation
|
var operation struct {
|
||||||
if err := e.BindBody(&patches); err != nil {
|
Op string `json:"op"`
|
||||||
return e.BadRequestError("Failed to parse JSON Patch data", err)
|
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 patches
|
// Create event with single operation
|
||||||
incomingEvent := &Event{
|
incomingEvent := &Event{
|
||||||
ItemID: itemID,
|
ItemID: itemID,
|
||||||
Collection: collection,
|
Collection: collection,
|
||||||
Patches: patches,
|
Operation: operation.Op,
|
||||||
|
Path: operation.Path,
|
||||||
|
Value: operation.Value,
|
||||||
|
From: operation.From,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the event
|
// Process the event
|
||||||
@@ -47,8 +55,8 @@ func setupAPIRoutes(app core.App, eventStore *SimpleEventStore) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if incomingEvent.ItemID == "" || incomingEvent.Collection == "" || len(incomingEvent.Patches) == 0 {
|
if incomingEvent.ItemID == "" || incomingEvent.Collection == "" || incomingEvent.Operation == "" {
|
||||||
return e.BadRequestError("Missing required fields: item_id, collection, patches", nil)
|
return e.BadRequestError("Missing required fields: item_id, collection, operation", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the event
|
// Process the event
|
||||||
@@ -134,8 +142,8 @@ func setupAPIRoutes(app core.App, eventStore *SimpleEventStore) {
|
|||||||
processedEvents := make([]Event, 0, len(events))
|
processedEvents := make([]Event, 0, len(events))
|
||||||
for _, incomingEvent := range events {
|
for _, incomingEvent := range events {
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if incomingEvent.ItemID == "" || incomingEvent.Collection == "" || len(incomingEvent.Patches) == 0 {
|
if incomingEvent.ItemID == "" || incomingEvent.Collection == "" || incomingEvent.Operation == "" {
|
||||||
return e.BadRequestError("Missing required fields in event: item_id, collection, patches", nil)
|
return e.BadRequestError("Missing required fields in event: item_id, collection, operation", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
processedEvent, err := eventStore.ProcessEvent(&incomingEvent)
|
processedEvent, err := eventStore.ProcessEvent(&incomingEvent)
|
||||||
|
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -23,40 +22,32 @@ func NewSimpleEventStore(app *pocketbase.PocketBase) *SimpleEventStore {
|
|||||||
|
|
||||||
// GetLatestEvent returns the latest event from the event log
|
// GetLatestEvent returns the latest event from the event log
|
||||||
func (es *SimpleEventStore) GetLatestEvent() (*Event, error) {
|
func (es *SimpleEventStore) GetLatestEvent() (*Event, error) {
|
||||||
rows, err := es.app.DB().
|
records, err := es.app.FindRecordsByFilter("events", "", "-seq", 1, 0, map[string]any{})
|
||||||
Select("seq", "hash", "item_id", "event_id", "collection", "data", "timestamp").
|
if err != nil || len(records) == 0 {
|
||||||
From("events").
|
|
||||||
OrderBy("seq DESC").
|
|
||||||
Limit(1).
|
|
||||||
Rows()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to query latest event: %w", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
if !rows.Next() {
|
|
||||||
return nil, nil // No events found
|
return nil, nil // No events found
|
||||||
}
|
}
|
||||||
|
|
||||||
var event Event
|
record := records[0]
|
||||||
var dataStr string
|
event := &Event{
|
||||||
|
Seq: record.GetInt("seq"),
|
||||||
err = rows.Scan(&event.Seq, &event.Hash, &event.ItemID, &event.EventID, &event.Collection, &dataStr, &event.Timestamp)
|
Hash: record.GetString("hash"),
|
||||||
if err != nil {
|
ItemID: record.GetString("item_id"),
|
||||||
return nil, fmt.Errorf("failed to scan latest event: %w", err)
|
EventID: record.GetString("event_id"),
|
||||||
|
Collection: record.GetString("collection"),
|
||||||
|
Operation: record.GetString("operation"),
|
||||||
|
Path: record.GetString("path"),
|
||||||
|
Value: record.GetString("value"),
|
||||||
|
From: record.GetString("from"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse patches from data field
|
// Parse timestamp
|
||||||
if dataStr != "" {
|
if timestampStr := record.GetString("timestamp"); timestampStr != "" {
|
||||||
var patches []PatchOperation
|
if timestamp, err := time.Parse(time.RFC3339, timestampStr); err == nil {
|
||||||
if err := json.Unmarshal([]byte(dataStr), &patches); err != nil {
|
event.Timestamp = timestamp
|
||||||
return nil, fmt.Errorf("failed to unmarshal patches: %w", err)
|
|
||||||
}
|
}
|
||||||
event.Patches = patches
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &event, nil
|
return event, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcessEvent processes an incoming event and applies it to the store
|
// ProcessEvent processes an incoming event and applies it to the store
|
||||||
@@ -71,7 +62,10 @@ func (es *SimpleEventStore) ProcessEvent(incomingEvent *Event) (*Event, error) {
|
|||||||
event := &Event{
|
event := &Event{
|
||||||
ItemID: incomingEvent.ItemID,
|
ItemID: incomingEvent.ItemID,
|
||||||
Collection: incomingEvent.Collection,
|
Collection: incomingEvent.Collection,
|
||||||
Patches: incomingEvent.Patches,
|
Operation: incomingEvent.Operation,
|
||||||
|
Path: incomingEvent.Path,
|
||||||
|
Value: incomingEvent.Value,
|
||||||
|
From: incomingEvent.From,
|
||||||
EventID: uuid.New().String(),
|
EventID: uuid.New().String(),
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
}
|
}
|
||||||
@@ -116,42 +110,47 @@ func (es *SimpleEventStore) saveEvent(event *Event) error {
|
|||||||
record.Set("item_id", event.ItemID)
|
record.Set("item_id", event.ItemID)
|
||||||
record.Set("event_id", event.EventID)
|
record.Set("event_id", event.EventID)
|
||||||
record.Set("collection", event.Collection)
|
record.Set("collection", event.Collection)
|
||||||
|
record.Set("operation", event.Operation)
|
||||||
|
record.Set("path", event.Path)
|
||||||
|
record.Set("value", event.Value)
|
||||||
|
record.Set("from", event.From)
|
||||||
record.Set("timestamp", event.Timestamp.Format(time.RFC3339))
|
record.Set("timestamp", event.Timestamp.Format(time.RFC3339))
|
||||||
|
|
||||||
// Convert patches to JSON string and store in data field
|
|
||||||
if event.Patches != nil {
|
|
||||||
patchesBytes, err := json.Marshal(event.Patches)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal event patches: %w", err)
|
|
||||||
}
|
|
||||||
record.Set("data", string(patchesBytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
return es.app.Save(record)
|
return es.app.Save(record)
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyEvent applies an event to the cached data using JSON Patch operations
|
// applyEvent applies a single operation to the cached data
|
||||||
func (es *SimpleEventStore) applyEvent(event *Event) error {
|
func (es *SimpleEventStore) applyEvent(event *Event) error {
|
||||||
// Get current document state
|
// Get current document state
|
||||||
currentDoc, err := es.getCurrentDocument(event.Collection, event.ItemID)
|
currentDoc, err := es.getCurrentDocument(event.Collection, event.ItemID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If document doesn't exist, create empty one for patches to work on
|
// If document doesn't exist, create empty one
|
||||||
currentDoc = map[string]interface{}{
|
currentDoc = map[string]interface{}{
|
||||||
"id": event.ItemID,
|
"id": event.ItemID,
|
||||||
"created_at": event.Timestamp,
|
|
||||||
"updated_at": event.Timestamp,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply JSON Patch operations
|
// Apply single operation
|
||||||
patcher := &JSONPatcher{}
|
patcher := &JSONPatcher{}
|
||||||
updatedDoc, err := patcher.ApplyPatches(currentDoc, event.Patches)
|
patches := []PatchOperation{{
|
||||||
|
Op: event.Operation,
|
||||||
|
Path: event.Path,
|
||||||
|
Value: event.Value,
|
||||||
|
From: event.From,
|
||||||
|
}}
|
||||||
|
|
||||||
|
updatedDoc, err := patcher.ApplyPatches(currentDoc, patches)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to apply patches: %w", err)
|
return fmt.Errorf("failed to apply operation: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always update the updated_at timestamp
|
// Update timestamp
|
||||||
updatedDoc["updated_at"] = event.Timestamp
|
updatedDoc["updated_at"] = event.Timestamp.Format(time.RFC3339)
|
||||||
|
|
||||||
|
// Set created_at if this is a new document
|
||||||
|
if _, exists := currentDoc["created_at"]; !exists {
|
||||||
|
updatedDoc["created_at"] = event.Timestamp.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
// Save the updated document
|
// Save the updated document
|
||||||
return es.saveDocument(event.Collection, event.ItemID, updatedDoc)
|
return es.saveDocument(event.Collection, event.ItemID, updatedDoc)
|
||||||
@@ -230,38 +229,31 @@ func (es *SimpleEventStore) saveDocument(collectionName, itemID string, doc map[
|
|||||||
|
|
||||||
// GetEventsSince returns events since the given sequence number
|
// GetEventsSince returns events since the given sequence number
|
||||||
func (es *SimpleEventStore) GetEventsSince(seq int) ([]Event, error) {
|
func (es *SimpleEventStore) GetEventsSince(seq int) ([]Event, error) {
|
||||||
rows, err := es.app.DB().
|
records, err := es.app.FindRecordsByFilter("events", "seq > {:seq}", "seq", 1000, 0, map[string]any{"seq": seq})
|
||||||
Select("seq", "hash", "item_id", "event_id", "collection", "data", "timestamp").
|
|
||||||
From("events").
|
|
||||||
Where(dbx.NewExp("seq > {:seq}", map[string]any{"seq": seq})).
|
|
||||||
OrderBy("seq ASC").
|
|
||||||
Rows()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to fetch events: %w", err)
|
return nil, fmt.Errorf("failed to fetch events: %w", err)
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var events []Event
|
events := make([]Event, len(records))
|
||||||
for rows.Next() {
|
for i, record := range records {
|
||||||
var event Event
|
events[i] = Event{
|
||||||
var dataStr string
|
Seq: record.GetInt("seq"),
|
||||||
|
Hash: record.GetString("hash"),
|
||||||
err := rows.Scan(&event.Seq, &event.Hash, &event.ItemID, &event.EventID, &event.Collection, &dataStr, &event.Timestamp)
|
ItemID: record.GetString("item_id"),
|
||||||
if err != nil {
|
EventID: record.GetString("event_id"),
|
||||||
return nil, fmt.Errorf("failed to scan event: %w", err)
|
Collection: record.GetString("collection"),
|
||||||
|
Operation: record.GetString("operation"),
|
||||||
|
Path: record.GetString("path"),
|
||||||
|
Value: record.GetString("value"),
|
||||||
|
From: record.GetString("from"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse patches from data field
|
// Parse timestamp
|
||||||
if dataStr != "" {
|
if timestampStr := record.GetString("timestamp"); timestampStr != "" {
|
||||||
var patches []PatchOperation
|
if timestamp, err := time.Parse(time.RFC3339, timestampStr); err == nil {
|
||||||
if err := json.Unmarshal([]byte(dataStr), &patches); err != nil {
|
events[i].Timestamp = timestamp
|
||||||
return nil, fmt.Errorf("failed to unmarshal patches: %w", err)
|
|
||||||
}
|
}
|
||||||
event.Patches = patches
|
|
||||||
}
|
}
|
||||||
|
|
||||||
events = append(events, event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return events, nil
|
return events, nil
|
||||||
|
@@ -68,11 +68,14 @@ func (es *SimpleEventStore) MergeEventLog(cutoffDays int) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Rethink merging for single operations
|
||||||
consolidatedEvent := &Event{
|
consolidatedEvent := &Event{
|
||||||
Seq: nextSeq,
|
Seq: nextSeq,
|
||||||
ItemID: itemID,
|
ItemID: itemID,
|
||||||
Collection: collectionName,
|
Collection: collectionName,
|
||||||
Patches: patches,
|
Operation: "add", // Placeholder - merging needs redesign
|
||||||
|
Path: "/",
|
||||||
|
Value: "consolidated",
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -24,7 +24,7 @@ func setupCollections(app core.App) error {
|
|||||||
eventsCollection.UpdateRule = nil
|
eventsCollection.UpdateRule = nil
|
||||||
eventsCollection.DeleteRule = nil
|
eventsCollection.DeleteRule = nil
|
||||||
|
|
||||||
// Add fields
|
// Add fields - ONE EVENT = ONE OPERATION
|
||||||
eventsCollection.Fields.Add(&core.NumberField{
|
eventsCollection.Fields.Add(&core.NumberField{
|
||||||
Name: "seq",
|
Name: "seq",
|
||||||
Required: true,
|
Required: true,
|
||||||
@@ -46,7 +46,19 @@ func setupCollections(app core.App) error {
|
|||||||
Required: true,
|
Required: true,
|
||||||
})
|
})
|
||||||
eventsCollection.Fields.Add(&core.TextField{
|
eventsCollection.Fields.Add(&core.TextField{
|
||||||
Name: "data",
|
Name: "operation",
|
||||||
|
Required: true,
|
||||||
|
})
|
||||||
|
eventsCollection.Fields.Add(&core.TextField{
|
||||||
|
Name: "path",
|
||||||
|
Required: true,
|
||||||
|
})
|
||||||
|
eventsCollection.Fields.Add(&core.TextField{
|
||||||
|
Name: "value",
|
||||||
|
Required: false,
|
||||||
|
})
|
||||||
|
eventsCollection.Fields.Add(&core.TextField{
|
||||||
|
Name: "from",
|
||||||
Required: false,
|
Required: false,
|
||||||
})
|
})
|
||||||
eventsCollection.Fields.Add(&core.DateField{
|
eventsCollection.Fields.Add(&core.DateField{
|
||||||
|
18
types.go
18
types.go
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -20,13 +19,16 @@ type Event struct {
|
|||||||
EventID string `json:"event_id"`
|
EventID string `json:"event_id"`
|
||||||
// Collection of the item that is to be manipulated, defined by the client
|
// Collection of the item that is to be manipulated, defined by the client
|
||||||
Collection string `json:"collection"`
|
Collection string `json:"collection"`
|
||||||
// RFC6902 JSON Patch operations that define the changes
|
// Single operation - ONE EVENT = ONE OPERATION
|
||||||
Patches []PatchOperation `json:"patches"`
|
Operation string `json:"operation"` // add, remove, replace, move, copy, test
|
||||||
|
Path string `json:"path"` // JSON Pointer to target location
|
||||||
|
Value string `json:"value"` // Value as string (for add/replace operations)
|
||||||
|
From string `json:"from"` // Source path for move/copy operations
|
||||||
// Timestamp of the event - server generated, when the event was processed
|
// Timestamp of the event - server generated, when the event was processed
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PatchOperation represents a single RFC6902 JSON Patch operation
|
// PatchOperation represents a single RFC6902 JSON Patch operation (still needed for JSONPatcher)
|
||||||
type PatchOperation struct {
|
type PatchOperation struct {
|
||||||
Op string `json:"op"` // add, remove, replace, move, copy, test
|
Op string `json:"op"` // add, remove, replace, move, copy, test
|
||||||
Path string `json:"path"` // JSON Pointer to target location
|
Path string `json:"path"` // JSON Pointer to target location
|
||||||
@@ -62,12 +64,8 @@ type SyncResponse struct {
|
|||||||
func (e *Event) serialize() string {
|
func (e *Event) serialize() string {
|
||||||
timestamp := e.Timestamp.Format(time.RFC3339Nano)
|
timestamp := e.Timestamp.Format(time.RFC3339Nano)
|
||||||
|
|
||||||
// Convert patches to JSON string
|
return fmt.Sprintf("seq:%d|item_id:%s|event_id:%s|collection:%s|operation:%s|path:%s|value:%s|from:%s|timestamp:%s",
|
||||||
patchesBytes, _ := json.Marshal(e.Patches)
|
e.Seq, e.ItemID, e.EventID, e.Collection, e.Operation, e.Path, e.Value, e.From, timestamp)
|
||||||
patchesStr := string(patchesBytes)
|
|
||||||
|
|
||||||
return fmt.Sprintf("seq:%d|item_id:%s|event_id:%s|collection:%s|patches:%s|timestamp:%s",
|
|
||||||
e.Seq, e.ItemID, e.EventID, e.Collection, patchesStr, timestamp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate hash of event plus previous hash
|
// Calculate hash of event plus previous hash
|
||||||
|
Reference in New Issue
Block a user