package queryapi import ( "net/http" "time" "queryorchestration/internal/database/repository" "github.com/google/uuid" "github.com/labstack/echo/v4" "github.com/oapi-codegen/nullable" openapi_types "github.com/oapi-codegen/runtime/types" ) // CreateFolder creates a new folder for organizing documents. // POST /folders // Takes a FolderCreate request body containing path, clientId, parentId (optional), and createdBy. // Returns the created Folder on success, or an error response. func (s *Controllers) CreateFolder(ctx echo.Context) error { var req CreateFolderJSONRequestBody if err := ctx.Bind(&req); err != nil { return echo.NewHTTPError(http.StatusBadRequest, "invalid request body") } // Validate required fields if req.Path == "" { return echo.NewHTTPError(http.StatusBadRequest, "path is required") } if req.ClientId == "" { return echo.NewHTTPError(http.StatusBadRequest, "clientId is required") } if req.CreatedBy == "" { return echo.NewHTTPError(http.StatusBadRequest, "createdBy is required") } // Convert parent ID if provided var parentID *uuid.UUID if req.ParentId != nil { id := uuid.UUID(*req.ParentId) parentID = &id } folder, err := s.svc.Folder.CreateFolder(ctx.Request().Context(), req.Path, parentID, string(req.ClientId), string(req.CreatedBy)) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return ctx.JSON(http.StatusCreated, convertRepoFolderToAPI(folder)) } // RenameFolder updates the path of an existing folder. // PATCH /folders/{folderId} // Takes a FolderRename request body containing the new path. // Returns 204 No Content on success, or an error response. func (s *Controllers) RenameFolder(ctx echo.Context, folderId openapi_types.UUID) error { var req RenameFolderJSONRequestBody if err := ctx.Bind(&req); err != nil { return echo.NewHTTPError(http.StatusBadRequest, "invalid request body") } if req.Path == "" { return echo.NewHTTPError(http.StatusBadRequest, "path is required") } err := s.svc.Folder.RenameFolder(ctx.Request().Context(), uuid.UUID(folderId), req.Path) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return ctx.NoContent(http.StatusNoContent) } // GetFolderDocuments retrieves all documents in a specific folder with enriched metadata. // GET /folders/{folderId}/documents // Parameters: // - folderId: UUID of the folder // - params: Query parameters including optional textRecord flag // // When textRecord=true, includes the full field extraction response for each document. // Returns a list of DocumentInFolder objects (lighter format excluding client_id and computed fields). func (s *Controllers) GetFolderDocuments(ctx echo.Context, folderId openapi_types.UUID, params GetFolderDocumentsParams) error { includeTextRecord := params.TextRecord != nil && *params.TextRecord documents, err := s.svc.Folder.GetDocumentsEnriched(ctx.Request().Context(), uuid.UUID(folderId), includeTextRecord) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } // Convert to API response format apiDocs := make([]DocumentInFolder, len(documents)) for i, doc := range documents { // Convert labels to API format labels := make([]LabelRecord, len(doc.Labels)) for j, lbl := range doc.Labels { labels[j] = LabelRecord{ Id: openapi_types.UUID(lbl.ID), DocumentId: DocumentID(lbl.DocumentID), Label: lbl.Label, AppliedBy: openapi_types.Email(lbl.AppliedBy), AppliedAt: lbl.AppliedAt, } } apiDocs[i] = DocumentInFolder{ Id: DocumentID(doc.ID), Hash: Hash(doc.Hash), HasTextRecord: doc.HasTextRecord, Labels: labels, } if doc.FolderID != nil { apiDocs[i].FolderId = (*openapi_types.UUID)(doc.FolderID) } if doc.Filename != nil { apiDocs[i].Filename = doc.Filename } if doc.OriginalPath != nil { apiDocs[i].OriginalPath = doc.OriginalPath } if doc.FileSizeBytes != nil { apiDocs[i].FileSizeBytes = doc.FileSizeBytes } // Include text record if available if doc.TextRecord != nil { current, ok := doc.TextRecord.SingleFields.(*repository.Currentfieldextraction) if ok && current != nil { arrayFields, ok := doc.TextRecord.ArrayFields.([]*repository.Documentfieldextractionarrayfield) if ok { textRecord := buildFieldExtractionResponse(current, arrayFields) apiDocs[i].TextRecord = textRecord } } } } return ctx.JSON(http.StatusOK, map[string]interface{}{ "documents": apiDocs, }) } // ListClientFolders retrieves all folders for a client. // GET /client/{id}/folders // Parameters: // - id: Client ID // - params: Query parameters including optional metrics flag // // Returns a FolderList containing all folders for the client. // Each folder includes its ID, path, and parentId, allowing clients to reconstruct the folder hierarchy. // Root-level folders will have parentId set to null. // When params.Metrics is true, each folder includes processing metrics (document counts by label). func (s *Controllers) ListClientFolders(ctx echo.Context, id ClientID, params ListClientFoldersParams) error { folders, err := s.svc.Folder.GetFoldersByClient(ctx.Request().Context(), string(id)) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } // Convert repository folders to API folders apiFolders := make([]Folder, len(folders)) for i, f := range folders { apiFolders[i] = convertRepoFolderToAPI(f) // Populate metrics if requested if params.Metrics != nil && *params.Metrics { metrics, metricsErr := s.svc.Folder.GetFolderMetrics(ctx.Request().Context(), f.ID) if metricsErr != nil { // Log error but continue - metrics are optional enhancement continue } apiFolders[i].Metrics = nullable.NewNullableWithValue(FolderMetricsInline{ TotalDocuments: metrics.TotalDocuments, ByLabel: metrics.ByLabel, }) } } return ctx.JSON(http.StatusOK, FolderList{ Folders: apiFolders, }) } // GetFolderMetrics retrieves processing metrics for a folder including document counts by label. // GET /folders/{folderId}/metrics // Returns FolderMetrics containing total document count and breakdown by label. func (s *Controllers) GetFolderMetrics(ctx echo.Context, folderId openapi_types.UUID) error { metrics, err := s.svc.Folder.GetFolderMetrics(ctx.Request().Context(), uuid.UUID(folderId)) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return ctx.JSON(http.StatusOK, FolderMetrics{ FolderId: folderId, TotalDocuments: metrics.TotalDocuments, ByLabel: metrics.ByLabel, }) } // convertRepoFolderToAPI converts a repository Folder to the API Folder type. func convertRepoFolderToAPI(folder *repository.Folder) Folder { var createdAt time.Time if folder.Createdat.Valid { createdAt = folder.Createdat.Time } result := Folder{ Id: openapi_types.UUID(folder.ID), Path: folder.Path, ClientId: ClientID(folder.Clientid), CreatedBy: openapi_types.Email(folder.Createdby), CreatedAt: createdAt, } if folder.Parentid != nil { result.ParentId = nullable.NewNullableWithValue(openapi_types.UUID(*folder.Parentid)) } return result }