package httpx import ( "encoding/json" "errors" "io" "net/http" "strings" bootstrapservice "moku-backend/internal/bootstrap" "github.com/go-chi/chi/v5" ) type createProjectFolderRequest struct { Name string `json:"name"` ParentFolderID string `json:"parentFolderId"` } 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) 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.ParentFolderID = strings.TrimSpace(payload.ParentFolderID) 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, ParentFolderID: payload.ParentFolderID, Name: payload.Name, }) if err != nil { routes.writeProjectFolderError(w, r, err) return } WriteJSON(w, http.StatusCreated, map[string]any{ "data": result, "meta": map[string]any{ "resource": "project-folder-create", "persisted": true, }, }) } func (routes apiRoutes) writeProjectFolderError(w http.ResponseWriter, r *http.Request, err error) { switch { case errors.Is(err, bootstrapservice.ErrProjectNotFound), errors.Is(err, bootstrapservice.ErrProjectFolderNotFound): WriteError(w, http.StatusNotFound, RequestIDFromContext(r.Context()), "not_found", err.Error()) default: routes.cfg.Logger.Error("persist project folder", "error", err, "path", r.URL.Path) message := "Failed to persist project folder." if routes.cfg.Config.IsDevelopment() { message = message + " " + err.Error() } WriteError(w, http.StatusInternalServerError, RequestIDFromContext(r.Context()), "project_folder_persist_failed", message) } } func decodeProjectFolderRequest(w http.ResponseWriter, r *http.Request) (createProjectFolderRequest, bool) { var payload createProjectFolderRequest decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(&payload); err != nil { if errors.Is(err, io.EOF) { WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_json", "The request body is required and must be valid JSON.") return payload, false } WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_json", "The request body must be valid JSON.") return payload, false } if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) { WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_json", "The request body must contain a single JSON object.") return payload, false } return payload, true }