feat: Add storage hierarchy API and frontend integration

This commit is contained in:
garionion (aider) 2025-02-28 21:18:07 +01:00 committed by garionion
parent 005b04e380
commit 29ec555090
6 changed files with 264 additions and 10 deletions

31
api/api.go Normal file
View file

@ -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)
}

193
api/storage.go Normal file
View file

@ -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)
}

View file

@ -53,3 +53,16 @@ SELECT * FROM pictures WHERE event_ID = ?;
-- name: AddPicture :exec -- name: AddPicture :exec
INSERT INTO pictures (user_ID, storagespace_ID, object_ID, event_ID, check_in_ID, Path, Description, datetime) INSERT INTO pictures (user_ID, storagespace_ID, object_ID, event_ID, check_in_ID, Path, Description, datetime)
VALUES (?, ?, ?, ?, ?, ?, ?, ?); 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 = ?;

27
main.go
View file

@ -14,12 +14,16 @@ import (
"os" "os"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"github.com/your-username/your-repo/api"
db "github.com/your-username/your-repo/database/sqlite/generated"
) )
//go:embed static/* //go:embed static/*
@ -61,7 +65,7 @@ var (
logger *zap.Logger logger *zap.Logger
) )
var db *sql.DB var sqlDB *sql.DB
func main() { func main() {
// Load configuration from YAML file, environment variables, and flags // Load configuration from YAML file, environment variables, and flags
@ -185,25 +189,28 @@ func main() {
cfg.DB.User, cfg.DB.Password, cfg.DB.Host, cfg.DB.Port, cfg.DB.DBName) 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 { if err != nil {
logger.Fatal("Failed to connect to database", zap.Error(err)) logger.Fatal("Failed to connect to database", zap.Error(err))
} }
defer db.Close() defer sqlDB.Close()
// Test the connection // 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.Fatal("Failed to ping database", zap.Error(err))
} }
logger.Info("Successfully connected to database", zap.String("driver", cfg.DB.Driver)) logger.Info("Successfully connected to database", zap.String("driver", cfg.DB.Driver))
if cfg.DB.Driver == "sqlite3" { if cfg.DB.Driver == "sqlite3" {
logger.Info("migrating sqlite database") 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)) logger.Fatal("Failed to migrate database", zap.Error(err))
} }
} }
// Initialize database queries
dbQueries := db.New(sqlDB)
// Initialize Echo // Initialize Echo
e := echo.New() e := echo.New()
e.Use(middleware.Logger()) e.Use(middleware.Logger())
@ -214,9 +221,12 @@ func main() {
e.GET("/oauth/login", handleOAuthLogin) e.GET("/oauth/login", handleOAuthLogin)
e.GET("/oauth/callback", handleOAuthCallback) e.GET("/oauth/callback", handleOAuthCallback)
// Initialize API
apiHandler := api.New(dbQueries, logger)
// Protected API routes // Protected API routes
api := e.Group("/api/v1", oauthMiddleware) apiGroup := e.Group("/api/v1", oauthMiddleware)
api.GET("/health", func(c echo.Context) error { apiGroup.GET("/health", func(c echo.Context) error {
user := c.Get("user").(map[string]interface{}) user := c.Get("user").(map[string]interface{})
logger.Info("Health check requested", zap.Any("user", user)) logger.Info("Health check requested", zap.Any("user", user))
return c.JSON(http.StatusOK, map[string]interface{}{ return c.JSON(http.StatusOK, map[string]interface{}{
@ -225,6 +235,9 @@ func main() {
}) })
}) })
// Register all other API routes
apiHandler.RegisterRoutes(e, oauthMiddleware)
// Serve static files - must be after API routes // Serve static files - must be after API routes
e.GET("/*", echo.WrapHandler(http.FileServer(http.FS(staticFiles)))) e.GET("/*", echo.WrapHandler(http.FileServer(http.FS(staticFiles))))

View file

@ -50,7 +50,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, watch, onMounted } from 'vue'; import { ref, computed, watch, onMounted, defineComponent } from 'vue';
import { useStorageStore } from '@/stores/storage'; import { useStorageStore } from '@/stores/storage';
// Component for recursive display of storage boxes // Component for recursive display of storage boxes
@ -68,7 +68,7 @@ const StorageBox = defineComponent({
const childStorages = computed(() => { const childStorages = computed(() => {
return props.nestedStorages.filter(storage => return props.nestedStorages.filter(storage =>
storage.parent && storage.parent === props.storage.id storage.parent && storage.parent.valid && storage.parent.int64 === props.storage.id
); );
}); });

View file

@ -41,7 +41,11 @@ export const useStorageStore = defineStore('storage', {
} }
const data = await response.json(); const data = await response.json();
this.storageSpaces = data; this.storageSpaces = data.map((space: any) => ({
id: space.id,
parent: space.parent,
location: space.location
}));
} catch (error: any) { } catch (error: any) {
this.error = error.message; this.error = error.message;
throw error; throw error;