Files
query-orchestration/api/queryAPI/folders.go
T

219 lines
7.1 KiB
Go
Raw Normal View History

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
}