feat: Add storage hierarchy API and frontend integration
This commit is contained in:
parent
005b04e380
commit
29ec555090
6 changed files with 264 additions and 10 deletions
31
api/api.go
Normal file
31
api/api.go
Normal 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
193
api/storage.go
Normal 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)
|
||||||
|
}
|
|
@ -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
27
main.go
|
@ -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,24 +189,27 @@ 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()
|
||||||
|
@ -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{}{
|
||||||
|
@ -224,6 +234,9 @@ func main() {
|
||||||
"user": user,
|
"user": user,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 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))))
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Reference in a new issue