From 29ec5550902427415ca8229d546e28c4aede944c Mon Sep 17 00:00:00 2001 From: "garionion (aider)" Date: Fri, 28 Feb 2025 21:18:07 +0100 Subject: [PATCH] feat: Add storage hierarchy API and frontend integration --- api/api.go | 31 ++++ api/storage.go | 193 ++++++++++++++++++++++++ database/sqlite/query.sql | 13 ++ main.go | 27 +++- web/src/components/StorageHierarchy.vue | 4 +- web/src/stores/storage.ts | 6 +- 6 files changed, 264 insertions(+), 10 deletions(-) create mode 100644 api/api.go create mode 100644 api/storage.go diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..84bbfa3 --- /dev/null +++ b/api/api.go @@ -0,0 +1,31 @@ +package api + +import ( + "github.com/labstack/echo/v4" + "go.uber.org/zap" + + db "github.com/your-username/your-repo/database/sqlite/generated" +) + +// API holds all API handlers +type API struct { + Storage *StorageHandler + logger *zap.Logger +} + +// New creates a new API instance +func New(dbQueries *db.Queries, logger *zap.Logger) *API { + return &API{ + Storage: NewStorageHandler(dbQueries, logger), + logger: logger, + } +} + +// RegisterRoutes registers all API routes +func (a *API) RegisterRoutes(e *echo.Echo, middleware echo.MiddlewareFunc) { + // Create API group with middleware + api := e.Group("/api/v1", middleware) + + // Register routes for each handler + a.Storage.RegisterRoutes(api) +} diff --git a/api/storage.go b/api/storage.go new file mode 100644 index 0000000..8df2f58 --- /dev/null +++ b/api/storage.go @@ -0,0 +1,193 @@ +package api + +import ( + "context" + "database/sql" + "net/http" + "strconv" + + "github.com/labstack/echo/v4" + "go.uber.org/zap" + + db "github.com/your-username/your-repo/database/sqlite/generated" +) + +type StorageHandler struct { + db *db.Queries + logger *zap.Logger +} + +func NewStorageHandler(db *db.Queries, logger *zap.Logger) *StorageHandler { + return &StorageHandler{ + db: db, + logger: logger, + } +} + +// GetStorageSpaces returns all storage spaces +func (h *StorageHandler) GetStorageSpaces(c echo.Context) error { + ctx := context.Background() + + storageSpaces, err := h.db.GetAllStorageSpaces(ctx) + if err != nil { + h.logger.Error("Failed to fetch storage spaces", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "Failed to fetch storage spaces", + }) + } + + // Convert to a format suitable for the frontend + result := make([]map[string]interface{}, len(storageSpaces)) + for i, space := range storageSpaces { + result[i] = map[string]interface{}{ + "id": space.ID, + "parent": space.Parent, + "location": space.Location, + } + } + + return c.JSON(http.StatusOK, result) +} + +// GetObjectsInStorage returns all objects in a specific storage space +func (h *StorageHandler) GetObjectsInStorage(c echo.Context) error { + ctx := context.Background() + + // Parse storage ID from URL + storageIDStr := c.Param("id") + storageID, err := strconv.ParseInt(storageIDStr, 10, 64) + if err != nil { + h.logger.Warn("Invalid storage ID", zap.String("id", storageIDStr)) + return c.JSON(http.StatusBadRequest, map[string]string{ + "error": "Invalid storage ID", + }) + } + + // Check if storage exists + _, err = h.db.GetStorageSpaceByID(ctx, storageID) + if err != nil { + if err == sql.ErrNoRows { + return c.JSON(http.StatusNotFound, map[string]string{ + "error": "Storage space not found", + }) + } + h.logger.Error("Failed to fetch storage space", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "Failed to fetch storage space", + }) + } + + // Get objects in this storage + objects, err := h.db.GetObjectsByStorageID(ctx, storageID) + if err != nil { + h.logger.Error("Failed to fetch objects in storage", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "Failed to fetch objects in storage", + }) + } + + // Convert to a format suitable for the frontend + result := make([]map[string]interface{}, len(objects)) + for i, obj := range objects { + result[i] = map[string]interface{}{ + "id": obj.ID, + "name": obj.Name, + "storagespaceId": obj.StoragespaceID, + "description": obj.Description.String, + "serialnumber": obj.Serialnumber.String, + } + } + + return c.JSON(http.StatusOK, result) +} + +// GetStorageHierarchyObjects returns all objects in a storage hierarchy +func (h *StorageHandler) GetStorageHierarchyObjects(c echo.Context) error { + ctx := context.Background() + + // Parse root storage ID from URL + rootStorageIDStr := c.Param("id") + rootStorageID, err := strconv.ParseInt(rootStorageIDStr, 10, 64) + if err != nil { + h.logger.Warn("Invalid storage ID", zap.String("id", rootStorageIDStr)) + return c.JSON(http.StatusBadRequest, map[string]string{ + "error": "Invalid storage ID", + }) + } + + // Check if root storage exists + _, err = h.db.GetStorageSpaceByID(ctx, rootStorageID) + if err != nil { + if err == sql.ErrNoRows { + return c.JSON(http.StatusNotFound, map[string]string{ + "error": "Storage space not found", + }) + } + h.logger.Error("Failed to fetch storage space", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "Failed to fetch storage space", + }) + } + + // Get all storage spaces to build the hierarchy + allStorageSpaces, err := h.db.GetAllStorageSpaces(ctx) + if err != nil { + h.logger.Error("Failed to fetch all storage spaces", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "Failed to fetch storage hierarchy", + }) + } + + // Build a list of all storage IDs in the hierarchy + storageIDs := []int64{rootStorageID} + storageIDs = append(storageIDs, h.getChildStorageIDs(allStorageSpaces, rootStorageID)...) + + // Get objects for all storage spaces in the hierarchy + var allObjects []map[string]interface{} + + for _, storageID := range storageIDs { + // Get objects in this storage + objects, err := h.db.GetObjectsByStorageID(ctx, storageID) + if err != nil { + h.logger.Error("Failed to fetch objects in storage", + zap.Int64("storageID", storageID), + zap.Error(err)) + continue // Skip this storage if there's an error + } + + // Convert to a format suitable for the frontend + for _, obj := range objects { + allObjects = append(allObjects, map[string]interface{}{ + "id": obj.ID, + "name": obj.Name, + "storagespaceId": obj.StoragespaceID, + "description": obj.Description.String, + "serialnumber": obj.Serialnumber.String, + }) + } + } + + return c.JSON(http.StatusOK, allObjects) +} + +// Helper function to recursively get all child storage IDs +func (h *StorageHandler) getChildStorageIDs(spaces []db.Storagespace, parentID int64) []int64 { + var childIDs []int64 + + for _, space := range spaces { + if space.Parent.Valid && space.Parent.Int64 == parentID { + childIDs = append(childIDs, space.ID) + // Recursively get children of this child + childIDs = append(childIDs, h.getChildStorageIDs(spaces, space.ID)...) + } + } + + return childIDs +} + +// RegisterRoutes registers all storage-related routes +func (h *StorageHandler) RegisterRoutes(g *echo.Group) { + g.GET("/storageSpaces", h.GetStorageSpaces) + g.GET("/storageSpaces/:id/objects", h.GetObjectsInStorage) + g.GET("/storageSpaces/:id/hierarchy/objects", h.GetStorageHierarchyObjects) +} diff --git a/database/sqlite/query.sql b/database/sqlite/query.sql index 25d8ea7..673f764 100644 --- a/database/sqlite/query.sql +++ b/database/sqlite/query.sql @@ -53,3 +53,16 @@ SELECT * FROM pictures WHERE event_ID = ?; -- name: AddPicture :exec INSERT INTO pictures (user_ID, storagespace_ID, object_ID, event_ID, check_in_ID, Path, Description, datetime) VALUES (?, ?, ?, ?, ?, ?, ?, ?); + +-- name: GetAllStorageSpaces :many +SELECT id, parent, location FROM storagespace; + +-- name: GetStorageSpaceByID :one +SELECT id, parent, location FROM storagespace WHERE id = ?; + +-- name: GetChildStorageSpaces :many +SELECT id, parent, location FROM storagespace WHERE parent = ?; + +-- name: GetObjectsByStorageID :many +SELECT id, storagespace_id, name, description, serialnumber, created +FROM objects WHERE storagespace_ID = ?; diff --git a/main.go b/main.go index d2ad10b..ec778f7 100644 --- a/main.go +++ b/main.go @@ -14,12 +14,16 @@ import ( "os" "strconv" "strings" + "time" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" _ "github.com/mattn/go-sqlite3" "golang.org/x/oauth2" "gopkg.in/yaml.v2" + + "github.com/your-username/your-repo/api" + db "github.com/your-username/your-repo/database/sqlite/generated" ) //go:embed static/* @@ -61,7 +65,7 @@ var ( logger *zap.Logger ) -var db *sql.DB +var sqlDB *sql.DB func main() { // Load configuration from YAML file, environment variables, and flags @@ -185,24 +189,27 @@ func main() { cfg.DB.User, cfg.DB.Password, cfg.DB.Host, cfg.DB.Port, cfg.DB.DBName) } - db, err = sql.Open(cfg.DB.Driver, dsn) + sqlDB, err = sql.Open(cfg.DB.Driver, dsn) if err != nil { logger.Fatal("Failed to connect to database", zap.Error(err)) } - defer db.Close() + defer sqlDB.Close() // Test the connection - if err = db.Ping(); err != nil { + if err = sqlDB.Ping(); err != nil { logger.Fatal("Failed to ping database", zap.Error(err)) } logger.Info("Successfully connected to database", zap.String("driver", cfg.DB.Driver)) if cfg.DB.Driver == "sqlite3" { logger.Info("migrating sqlite database") - if err := migrateSqlite(db); err != nil { + if err := migrateSqlite(sqlDB); err != nil { logger.Fatal("Failed to migrate database", zap.Error(err)) } } + + // Initialize database queries + dbQueries := db.New(sqlDB) // Initialize Echo e := echo.New() @@ -214,9 +221,12 @@ func main() { e.GET("/oauth/login", handleOAuthLogin) e.GET("/oauth/callback", handleOAuthCallback) + // Initialize API + apiHandler := api.New(dbQueries, logger) + // Protected API routes - api := e.Group("/api/v1", oauthMiddleware) - api.GET("/health", func(c echo.Context) error { + apiGroup := e.Group("/api/v1", oauthMiddleware) + apiGroup.GET("/health", func(c echo.Context) error { user := c.Get("user").(map[string]interface{}) logger.Info("Health check requested", zap.Any("user", user)) return c.JSON(http.StatusOK, map[string]interface{}{ @@ -224,6 +234,9 @@ func main() { "user": user, }) }) + + // Register all other API routes + apiHandler.RegisterRoutes(e, oauthMiddleware) // Serve static files - must be after API routes e.GET("/*", echo.WrapHandler(http.FileServer(http.FS(staticFiles)))) diff --git a/web/src/components/StorageHierarchy.vue b/web/src/components/StorageHierarchy.vue index df5ddb9..96e4736 100644 --- a/web/src/components/StorageHierarchy.vue +++ b/web/src/components/StorageHierarchy.vue @@ -50,7 +50,7 @@