511 lines
15 KiB
Go
511 lines
15 KiB
Go
// Path: Backend/internal/httpx/api_project_routes.go
|
|
|
|
package httpx
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
bootstrapservice "moku-backend/internal/bootstrap"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
func (routes apiRoutes) handleProjectFolders(w http.ResponseWriter, r *http.Request) {
|
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
|
if projectID == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
|
|
return
|
|
}
|
|
|
|
folders, err := routes.bootstrapService().GetProjectHierarchyFolders(r.Context(), projectID)
|
|
if err != nil {
|
|
routes.writeProjectFolderError(w, r, err, "load")
|
|
return
|
|
}
|
|
|
|
WriteJSON(w, http.StatusOK, map[string]any{
|
|
"data": map[string]any{
|
|
"projectId": projectID,
|
|
"folders": folders,
|
|
},
|
|
"meta": map[string]any{
|
|
"resource": "project-folders",
|
|
},
|
|
})
|
|
}
|
|
|
|
func (routes apiRoutes) handleCreateProjectFolder(w http.ResponseWriter, r *http.Request) {
|
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
|
if projectID == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
|
|
return
|
|
}
|
|
|
|
payload, ok := decodeProjectFolderRequest(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
payload.Name = strings.TrimSpace(payload.Name)
|
|
payload.ParentFolderPath = strings.TrimSpace(payload.ParentFolderPath)
|
|
if payload.Name == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Folder name is required.")
|
|
return
|
|
}
|
|
|
|
result, err := routes.bootstrapService().CreateProjectFolder(r.Context(), bootstrapservice.CreateProjectFolderInput{
|
|
ProjectID: projectID,
|
|
ParentFolderPath: payload.ParentFolderPath,
|
|
Name: payload.Name,
|
|
})
|
|
if err != nil {
|
|
routes.writeProjectFolderError(w, r, err, "persist")
|
|
return
|
|
}
|
|
|
|
WriteJSON(w, http.StatusCreated, map[string]any{
|
|
"data": result,
|
|
"meta": map[string]any{
|
|
"resource": "project-folder-create",
|
|
"persisted": true,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (routes apiRoutes) handleDeleteProjectFolder(w http.ResponseWriter, r *http.Request) {
|
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
|
if projectID == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
|
|
return
|
|
}
|
|
|
|
payload := decodeDeleteProjectFolderRequest(r)
|
|
if strings.TrimSpace(payload.FolderPath) == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Folder ID is required.")
|
|
return
|
|
}
|
|
|
|
result, err := routes.bootstrapService().DeleteProjectFolder(r.Context(), bootstrapservice.DeleteProjectFolderInput{
|
|
ProjectID: projectID,
|
|
FolderPath: payload.FolderPath,
|
|
})
|
|
if err != nil {
|
|
routes.writeProjectFolderError(w, r, err, "delete")
|
|
return
|
|
}
|
|
|
|
WriteJSON(w, http.StatusOK, map[string]any{
|
|
"data": result,
|
|
"meta": map[string]any{
|
|
"resource": "project-folder-delete",
|
|
"persisted": true,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (routes apiRoutes) handleRenameProjectFolder(w http.ResponseWriter, r *http.Request) {
|
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
|
if projectID == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
|
|
return
|
|
}
|
|
|
|
payload, ok := decodeRenameProjectFolderRequest(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
payload.FolderPath = strings.TrimSpace(payload.FolderPath)
|
|
payload.Name = strings.TrimSpace(payload.Name)
|
|
if payload.FolderPath == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Folder ID is required.")
|
|
return
|
|
}
|
|
if payload.Name == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Folder name is required.")
|
|
return
|
|
}
|
|
|
|
result, err := routes.bootstrapService().RenameProjectFolder(r.Context(), bootstrapservice.RenameProjectFolderInput{
|
|
ProjectID: projectID,
|
|
FolderPath: payload.FolderPath,
|
|
Name: payload.Name,
|
|
})
|
|
if err != nil {
|
|
routes.writeProjectFolderError(w, r, err, "rename")
|
|
return
|
|
}
|
|
|
|
WriteJSON(w, http.StatusOK, map[string]any{
|
|
"data": result,
|
|
"meta": map[string]any{
|
|
"resource": "project-folder-rename",
|
|
"persisted": true,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (routes apiRoutes) handleMoveProjectFolder(w http.ResponseWriter, r *http.Request) {
|
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
|
if projectID == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
|
|
return
|
|
}
|
|
|
|
payload, ok := decodeMoveProjectFolderRequest(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
payload.FolderPath = strings.TrimSpace(payload.FolderPath)
|
|
payload.FolderStableID = strings.TrimSpace(payload.FolderStableID)
|
|
payload.ParentFolderPath = strings.TrimSpace(payload.ParentFolderPath)
|
|
payload.ParentStableID = strings.TrimSpace(payload.ParentStableID)
|
|
if payload.FolderPath == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Folder ID is required.")
|
|
return
|
|
}
|
|
|
|
result, err := routes.bootstrapService().MoveProjectFolder(r.Context(), bootstrapservice.MoveProjectFolderInput{
|
|
ProjectID: projectID,
|
|
FolderPath: payload.FolderPath,
|
|
FolderStableID: payload.FolderStableID,
|
|
ParentFolderPath: payload.ParentFolderPath,
|
|
ParentStableID: payload.ParentStableID,
|
|
TargetIndex: payload.TargetIndex,
|
|
})
|
|
if err != nil {
|
|
routes.writeProjectFolderError(w, r, err, "move")
|
|
return
|
|
}
|
|
|
|
WriteJSON(w, http.StatusOK, map[string]any{
|
|
"data": result,
|
|
"meta": map[string]any{
|
|
"resource": "project-folder-move",
|
|
"persisted": true,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (routes apiRoutes) handleProjectTreeFolders(w http.ResponseWriter, r *http.Request) {
|
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
|
if projectID == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
|
|
return
|
|
}
|
|
|
|
folders, err := routes.bootstrapService().GetProjectTreeFolders(r.Context(), projectID)
|
|
if err != nil {
|
|
routes.writeProjectFolderError(w, r, err, "load")
|
|
return
|
|
}
|
|
|
|
WriteJSON(w, http.StatusOK, map[string]any{
|
|
"data": map[string]any{
|
|
"projectId": projectID,
|
|
"folders": folders,
|
|
},
|
|
"meta": map[string]any{
|
|
"resource": "project-tree-folders",
|
|
},
|
|
})
|
|
}
|
|
|
|
func (routes apiRoutes) handleProjectTree(w http.ResponseWriter, r *http.Request) {
|
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
|
if projectID == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
|
|
return
|
|
}
|
|
|
|
nodes, err := routes.bootstrapService().GetProjectTreeNodes(r.Context(), projectID)
|
|
if err != nil {
|
|
routes.writeProjectTreeError(w, r, err, "load")
|
|
return
|
|
}
|
|
|
|
WriteJSON(w, http.StatusOK, map[string]any{
|
|
"data": map[string]any{
|
|
"projectId": projectID,
|
|
"nodes": nodes,
|
|
},
|
|
"meta": map[string]any{
|
|
"resource": "project-tree",
|
|
},
|
|
})
|
|
}
|
|
|
|
func (routes apiRoutes) handleCreateProjectTreeFolder(w http.ResponseWriter, r *http.Request) {
|
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
|
if projectID == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
|
|
return
|
|
}
|
|
|
|
payload, ok := decodeProjectFolderRequest(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
payload.Name = strings.TrimSpace(payload.Name)
|
|
payload.ParentFolderPath = strings.TrimSpace(payload.ParentFolderPath)
|
|
if payload.Name == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Folder name is required.")
|
|
return
|
|
}
|
|
|
|
result, err := routes.bootstrapService().CreateProjectTreeFolder(r.Context(), bootstrapservice.CreateProjectFolderInput{
|
|
ProjectID: projectID,
|
|
ParentFolderPath: payload.ParentFolderPath,
|
|
Name: payload.Name,
|
|
})
|
|
if err != nil {
|
|
routes.writeProjectFolderError(w, r, err, "persist")
|
|
return
|
|
}
|
|
|
|
WriteJSON(w, http.StatusCreated, map[string]any{
|
|
"data": result,
|
|
"meta": map[string]any{
|
|
"resource": "project-tree-folder-create",
|
|
"persisted": true,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (routes apiRoutes) handleDeleteProjectTreeFolder(w http.ResponseWriter, r *http.Request) {
|
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
|
if projectID == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
|
|
return
|
|
}
|
|
|
|
payload := decodeDeleteProjectFolderRequest(r)
|
|
if strings.TrimSpace(payload.FolderPath) == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Folder ID is required.")
|
|
return
|
|
}
|
|
|
|
result, err := routes.bootstrapService().DeleteProjectTreeFolder(r.Context(), bootstrapservice.DeleteProjectFolderInput{
|
|
ProjectID: projectID,
|
|
FolderPath: payload.FolderPath,
|
|
})
|
|
if err != nil {
|
|
routes.writeProjectFolderError(w, r, err, "delete")
|
|
return
|
|
}
|
|
|
|
WriteJSON(w, http.StatusOK, map[string]any{
|
|
"data": result,
|
|
"meta": map[string]any{
|
|
"resource": "project-tree-folder-delete",
|
|
"persisted": true,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (routes apiRoutes) handleRenameProjectTreeFolder(w http.ResponseWriter, r *http.Request) {
|
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
|
if projectID == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
|
|
return
|
|
}
|
|
|
|
payload, ok := decodeRenameProjectFolderRequest(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
payload.FolderPath = strings.TrimSpace(payload.FolderPath)
|
|
payload.Name = strings.TrimSpace(payload.Name)
|
|
if payload.FolderPath == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Folder ID is required.")
|
|
return
|
|
}
|
|
if payload.Name == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Folder name is required.")
|
|
return
|
|
}
|
|
|
|
result, err := routes.bootstrapService().RenameProjectTreeFolder(r.Context(), bootstrapservice.RenameProjectFolderInput{
|
|
ProjectID: projectID,
|
|
FolderPath: payload.FolderPath,
|
|
Name: payload.Name,
|
|
})
|
|
if err != nil {
|
|
routes.writeProjectFolderError(w, r, err, "rename")
|
|
return
|
|
}
|
|
|
|
WriteJSON(w, http.StatusOK, map[string]any{
|
|
"data": result,
|
|
"meta": map[string]any{
|
|
"resource": "project-tree-folder-rename",
|
|
"persisted": true,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (routes apiRoutes) handleMoveProjectTreeFolder(w http.ResponseWriter, r *http.Request) {
|
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
|
if projectID == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
|
|
return
|
|
}
|
|
|
|
payload, ok := decodeMoveProjectFolderRequest(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
payload.FolderPath = strings.TrimSpace(payload.FolderPath)
|
|
payload.FolderStableID = strings.TrimSpace(payload.FolderStableID)
|
|
payload.ParentFolderPath = strings.TrimSpace(payload.ParentFolderPath)
|
|
payload.ParentStableID = strings.TrimSpace(payload.ParentStableID)
|
|
if payload.FolderPath == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Folder ID is required.")
|
|
return
|
|
}
|
|
|
|
result, err := routes.bootstrapService().MoveProjectTreeFolder(r.Context(), bootstrapservice.MoveProjectFolderInput{
|
|
ProjectID: projectID,
|
|
FolderPath: payload.FolderPath,
|
|
FolderStableID: payload.FolderStableID,
|
|
ParentFolderPath: payload.ParentFolderPath,
|
|
ParentStableID: payload.ParentStableID,
|
|
TargetIndex: payload.TargetIndex,
|
|
})
|
|
if err != nil {
|
|
routes.writeProjectFolderError(w, r, err, "move")
|
|
return
|
|
}
|
|
|
|
WriteJSON(w, http.StatusOK, map[string]any{
|
|
"data": result,
|
|
"meta": map[string]any{
|
|
"resource": "project-tree-folder-move",
|
|
"persisted": true,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (routes apiRoutes) handleCreateProjectTreeItem(w http.ResponseWriter, r *http.Request) {
|
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
|
if projectID == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
|
|
return
|
|
}
|
|
|
|
payload, ok := decodeProjectItemRequest(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
payload.Name = strings.TrimSpace(payload.Name)
|
|
payload.ParentFolderPath = strings.TrimSpace(payload.ParentFolderPath)
|
|
payload.ItemType = strings.TrimSpace(payload.ItemType)
|
|
if payload.Name == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Item name is required.")
|
|
return
|
|
}
|
|
if payload.ItemType == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Item type is required.")
|
|
return
|
|
}
|
|
|
|
result, err := routes.bootstrapService().CreateProjectTreeItem(r.Context(), bootstrapservice.CreateProjectItemInput{
|
|
ProjectID: projectID,
|
|
ParentFolderPath: payload.ParentFolderPath,
|
|
Name: payload.Name,
|
|
ItemType: payload.ItemType,
|
|
})
|
|
if err != nil {
|
|
routes.writeProjectTreeError(w, r, err, "persist")
|
|
return
|
|
}
|
|
|
|
WriteJSON(w, http.StatusCreated, map[string]any{
|
|
"data": result,
|
|
"meta": map[string]any{
|
|
"resource": "project-tree-item-create",
|
|
"persisted": true,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (routes apiRoutes) handleDeleteProjectTreeItem(w http.ResponseWriter, r *http.Request) {
|
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
|
if projectID == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
|
|
return
|
|
}
|
|
|
|
payload := decodeDeleteProjectItemRequest(r)
|
|
if strings.TrimSpace(payload.ItemPath) == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Item ID is required.")
|
|
return
|
|
}
|
|
|
|
result, err := routes.bootstrapService().DeleteProjectTreeItem(r.Context(), bootstrapservice.DeleteProjectItemInput{
|
|
ProjectID: projectID,
|
|
ItemPath: payload.ItemPath,
|
|
})
|
|
if err != nil {
|
|
routes.writeProjectTreeError(w, r, err, "delete")
|
|
return
|
|
}
|
|
|
|
WriteJSON(w, http.StatusOK, map[string]any{
|
|
"data": result,
|
|
"meta": map[string]any{
|
|
"resource": "project-tree-item-delete",
|
|
"persisted": true,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (routes apiRoutes) handleMoveProjectTreeItem(w http.ResponseWriter, r *http.Request) {
|
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
|
if projectID == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
|
|
return
|
|
}
|
|
|
|
payload, ok := decodeMoveProjectItemRequest(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
payload.ItemPath = strings.TrimSpace(payload.ItemPath)
|
|
payload.ItemStableID = strings.TrimSpace(payload.ItemStableID)
|
|
payload.ParentFolderPath = strings.TrimSpace(payload.ParentFolderPath)
|
|
payload.ParentStableID = strings.TrimSpace(payload.ParentStableID)
|
|
if payload.ItemPath == "" {
|
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Item ID is required.")
|
|
return
|
|
}
|
|
|
|
result, err := routes.bootstrapService().MoveProjectTreeItem(r.Context(), bootstrapservice.MoveProjectItemInput{
|
|
ProjectID: projectID,
|
|
ItemPath: payload.ItemPath,
|
|
ItemStableID: payload.ItemStableID,
|
|
ParentFolderPath: payload.ParentFolderPath,
|
|
ParentStableID: payload.ParentStableID,
|
|
TargetIndex: payload.TargetIndex,
|
|
})
|
|
if err != nil {
|
|
routes.writeProjectTreeError(w, r, err, "move")
|
|
return
|
|
}
|
|
|
|
WriteJSON(w, http.StatusOK, map[string]any{
|
|
"data": result,
|
|
"meta": map[string]any{
|
|
"resource": "project-tree-item-move",
|
|
"persisted": true,
|
|
},
|
|
})
|
|
}
|