Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
package main

import (
	"fmt"
	"log"

	"github.com/pocketbase/dbx"
	"github.com/pocketbase/pocketbase/core"
	"golang.org/x/exp/slices"
)

// TODO: make sure these errors surface as HTTP responses
// update this logic to look for primary org and then parent orgs (i.e. inherited books)

type Change struct {
	Collection string         `json:"collection"`
	Operation  string         `json:"operation"`
	Data       map[string]any `json:"data"`
	ID         string         `json:"id,omitempty"` // for update/delete operations
}

func ProcessChangeset(app core.App, changes []Change, orgId string, bookId string) error {
	// Start a transaction for all changes
	return app.RunInTransaction(func(txApp core.App) error {
		for _, change := range changes {
			if err := processChange(txApp, change, orgId, bookId); err != nil {
				return fmt.Errorf("failed to process change for collection %s: %w", change.Collection, err)
			}
		}
		return nil
	})
}

func processChange(app core.App, change Change, orgId string, bookId string) error {
	collection, err := app.FindCollectionByNameOrId(change.Collection)
	if err != nil {
		return fmt.Errorf("collection not found: %s", change.Collection)
	}

	allowedCollections := []string{"menus", "offers", "menuTiers", "problems", "problemTags", "checklists", "costsMaterial", "costsTime", "tierSets", "tiers", "formulas", "contentItems", "menuCopy", "warrantyCopy"}
	allowedCollection := slices.Contains(allowedCollections, collection.Name)

	if !allowedCollection {
		return fmt.Errorf("collection not found: %s", change.Collection)
	}

	switch change.Operation {
	case "create":
		return createRecord(app, collection, change.Data, orgId, bookId)
	case "update":
		return updateRecord(app, collection, change.Data, orgId, bookId)
	case "delete":
		return deleteRecord(app, collection, change.Data, orgId)
	default:
		return fmt.Errorf("unsupported operation: %s", change.Operation)
	}
}

func createRecord(app core.App, collection *core.Collection, data map[string]any, orgId string, bookId string) error {
	record := core.NewRecord(collection)

	globalCollections := []string{"currencies"}

	warrantyCopy, err := app.FindCollectionByNameOrId("warrantyCopy")
	if err != nil {
		return err
	}

	warranties, err := app.FindAllRecords(warrantyCopy)

	if err != nil {
		return err
	}
	log.Println("Query for all warranties")
	for _, warranty := range warranties {
		log.Println("..", warranty)
	}

	if refsRaw, ok := data["refs"]; ok {
		if refsSlice, ok := refsRaw.([]any); ok {
			for _, refItem := range refsSlice {
				if refMap, ok := refItem.(map[string]any); ok {
					collectionName, _ := refMap["collection"].(string)

					refIds, _ := refMap["refIds"].([]any)
					var recordIds []string

					for _, refId := range refIds {
						if _, ok := refId.(string); ok {
							var refRecord *core.Record
							var err error
							if slices.Contains(globalCollections, collectionName) {
								refRecord, err = app.FindFirstRecordByFilter(collectionName, "refId = {:refId}", dbx.Params{"refId": refId})
							} else {
								refRecord, err = app.FindFirstRecordByFilter(collectionName, "refId = {:refId} && org = {:orgId}", dbx.Params{"refId": refId, "orgId": orgId})

							}

							if err != nil {
								log.Println("Not found", refId)
								return err
							}

							recordIds = append(recordIds, refRecord.Id)
						}
					}

					record.Set(collectionName, recordIds)
				} else {
					log.Println("Invalid ref item format")
				}
			}
		} else {
			log.Println("Invalid ref format: expected array")
		}
	}

	// Set the data fields
	for key, value := range data {
		if key != "refs" { // skip ref since we handled it above
			record.Set(key, value)
		}
	}

	record.Set("org", orgId)
	record.Set("book", bookId)

	log.Println("collection", collection.Name)

	// TODO: we will probably have other types of tags to add here,
	// e.g. tags for content items
	// if collection.Name == "problemTags" {
	// 	cleanTagName(record)
	// }

	// Validate and save
	if err := app.Validate(record); err != nil {
		return fmt.Errorf("validation failed: %w", err)
	}

	if err := app.Save(record); err != nil {
		return fmt.Errorf("save failed: %w", err)
	}

	log.Printf("Created record in %s: %s", collection.Name, record.Id)
	return nil
}

func updateRecord(app core.App, collection *core.Collection, data map[string]any, orgId string, bookId string) error {
	// record, err := app.FindFirstRecordByData(collection, "refId", data["refId"])
	record, err := app.FindFirstRecordByFilter(collection, "refId = {:refId} && org = {:org}", dbx.Params{
		"refId": data["refId"],
		"org":   orgId,
	})
	if err != nil {
		return createRecord(app, collection, data, orgId, bookId)
		// return fmt.Errorf("record not found: %s", data["refId"])
	}

	for key, value := range data {
		record.Set(key, value)
	}

	record.Set("org", orgId)
	record.Set("book", bookId)

	// TODO: we will probably have other types of tags to add here,
	// e.g. tags for content items
	// if collection.Name == "problemTags" {
	// 	cleanTagName(record)
	// }

	if err := app.Validate(record); err != nil {
		return fmt.Errorf("validation failed: %w", err)
	}

	if err := app.Save(record); err != nil {
		return fmt.Errorf("save failed: %w", err)
	}

	log.Printf("Updated record in %s: %s", collection.Name, record.Id)
	return nil
}

func deleteRecord(app core.App, collection *core.Collection, data map[string]any, orgId string) error {
	// Find record by refId and org, similar to updateRecord
	record, err := app.FindFirstRecordByFilter(collection, "refId = {:refId} && org = {:org}", dbx.Params{
		"refId": data["refId"],
		"org":   orgId,
	})
	if err != nil {
		return fmt.Errorf("record not found: refId=%s, org=%s", data["refId"], orgId)
	}

	if err := app.Delete(record); err != nil {
		return fmt.Errorf("delete failed: %w", err)
	}

	log.Printf("Deleted record from %s: refId=%s", collection.Name, data["refId"])
	return nil
}

func onPricebookChanges(app core.App) {
	app.OnRecordAfterCreateSuccess("pricebookChanges").BindFunc(func(e *core.RecordEvent) error {
		// Skip changeset processing - just store the record without applying changes
		// This allows uploading changes without modifying the backend database
		log.Printf("Stored pricebookChanges record (processing skipped)")
		return e.Next()
	})
}