// 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, }, }) }