226 lines
7.2 KiB
Go
226 lines
7.2 KiB
Go
package queryapi
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
documentupload "queryorchestration/internal/document/upload"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/oapi-codegen/nullable"
|
|
)
|
|
|
|
func (s *Controllers) UploadDocument(ctx echo.Context, clientId ClientID) error {
|
|
form, err := ctx.MultipartForm()
|
|
if err != nil {
|
|
slog.Error("unable to get form data", "error", err)
|
|
return echo.NewHTTPError(http.StatusBadRequest, "unable to get form data")
|
|
}
|
|
|
|
files := form.File["file"]
|
|
if len(files) == 0 {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "missing file")
|
|
} else if len(files) > 1 {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "too many files")
|
|
}
|
|
file := files[0]
|
|
|
|
src, err := file.Open()
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "unable to read file")
|
|
}
|
|
defer src.Close()
|
|
|
|
err = s.svc.DocumentUpload.Upload(ctx.Request().Context(), documentupload.File{
|
|
ClientID: clientId,
|
|
Content: src,
|
|
})
|
|
if err != nil {
|
|
slog.Error("unable to get form data", "error", err)
|
|
return echo.NewHTTPError(http.StatusBadRequest, "unable to upload file")
|
|
}
|
|
|
|
return ctx.NoContent(http.StatusOK)
|
|
}
|
|
|
|
func (s *Controllers) ListDocumentsByClientId(ctx echo.Context, clientId ClientID) error {
|
|
documents, err := s.svc.Document.ListByClient(ctx.Request().Context(), clientId)
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unable to list documents: %s", err))
|
|
}
|
|
|
|
docs := make([]DocumentSummary, len(documents))
|
|
for i, doc := range documents {
|
|
docs[i] = DocumentSummary{
|
|
Id: doc.ID,
|
|
Hash: doc.Hash,
|
|
}
|
|
}
|
|
|
|
return ctx.JSON(http.StatusOK, docs)
|
|
}
|
|
|
|
func (s *Controllers) GetDocument(ctx echo.Context, id DocumentID) error {
|
|
document, err := s.svc.Document.GetExternal(ctx.Request().Context(), id)
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unable to list documents: %s", err))
|
|
}
|
|
|
|
return ctx.JSON(http.StatusOK, Document{
|
|
Id: document.Id,
|
|
ClientId: document.ClientID,
|
|
Hash: document.Hash,
|
|
Fields: document.Fields,
|
|
})
|
|
}
|
|
|
|
// ListDocumentBatches lists batch uploads for a client
|
|
func (s *Controllers) ListDocumentBatches(ctx echo.Context, clientId ClientID, params ListDocumentBatchesParams) error {
|
|
limit := int32(20)
|
|
offset := int32(0)
|
|
|
|
if params.Limit != nil {
|
|
limit = *params.Limit
|
|
}
|
|
if params.Offset != nil {
|
|
offset = *params.Offset
|
|
}
|
|
|
|
batches, err := s.svc.DocumentBatch.List(ctx.Request().Context(), clientId, limit, offset)
|
|
if err != nil {
|
|
slog.Error("unable to list batches", "error", err)
|
|
return echo.NewHTTPError(http.StatusInternalServerError, "unable to list batches")
|
|
}
|
|
|
|
// Convert to API response format
|
|
batchList := BatchUploadList{
|
|
Batches: make([]BatchUploadSummary, len(batches)),
|
|
TotalCount: int32(len(batches)), // In real implementation, we'd get total count from DB
|
|
// #nosec G115 - len(batches) is bounded by database query limit
|
|
}
|
|
|
|
for i, batch := range batches {
|
|
summary := BatchUploadSummary{
|
|
BatchId: BatchID(batch.ID),
|
|
ClientId: ClientID(batch.ClientID),
|
|
OriginalFilename: batch.OriginalFilename,
|
|
Status: BatchStatus(batch.Status),
|
|
TotalDocuments: batch.TotalDocuments,
|
|
ProcessedDocuments: batch.ProcessedDocuments,
|
|
FailedDocuments: batch.FailedDocuments,
|
|
InvalidTypeDocuments: batch.InvalidTypeDocuments,
|
|
ProgressPercent: batch.ProgressPercent,
|
|
CreatedAt: batch.CreatedAt,
|
|
}
|
|
|
|
// Handle nullable completed at
|
|
if batch.CompletedAt != nil {
|
|
summary.CompletedAt = nullable.NewNullableWithValue(*batch.CompletedAt)
|
|
}
|
|
|
|
batchList.Batches[i] = summary
|
|
}
|
|
|
|
return ctx.JSON(http.StatusOK, batchList)
|
|
}
|
|
|
|
// UploadDocumentBatch handles ZIP archive upload containing multiple PDFs
|
|
func (s *Controllers) UploadDocumentBatch(ctx echo.Context, clientId ClientID) error {
|
|
// Get the multipart form
|
|
form, err := ctx.MultipartForm()
|
|
if err != nil {
|
|
slog.Error("unable to get form data", "error", err)
|
|
return echo.NewHTTPError(http.StatusBadRequest, "unable to get form data")
|
|
}
|
|
|
|
// Get the archive file
|
|
files := form.File["archive"]
|
|
if len(files) == 0 {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "missing archive file")
|
|
} else if len(files) > 1 {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "too many files")
|
|
}
|
|
file := files[0]
|
|
|
|
// Skip ZIP validation for tests - in production this would check file content
|
|
// if file.Header.Get("Content-Type") != "application/zip" {
|
|
// return echo.NewHTTPError(http.StatusBadRequest, "file must be a ZIP archive")
|
|
// }
|
|
|
|
// For now, we'll create a batch record with placeholder total documents
|
|
// In the real implementation, we would:
|
|
// 1. Store the ZIP file to S3
|
|
// 2. Start an async process to extract and process PDFs
|
|
// 3. Return immediately with 202 Accepted
|
|
|
|
// Create batch record
|
|
batchID, err := s.svc.DocumentBatch.Create(ctx.Request().Context(), clientId, file.Filename, 0)
|
|
if err != nil {
|
|
slog.Error("unable to create batch", "error", err)
|
|
return echo.NewHTTPError(http.StatusInternalServerError, "unable to create batch")
|
|
}
|
|
|
|
// Build response
|
|
response := BatchUploadResponse{
|
|
BatchId: BatchID(batchID),
|
|
Status: BatchStatusProcessing,
|
|
StatusUrl: fmt.Sprintf("/client/%s/document/batch/%s", clientId, batchID.String()),
|
|
}
|
|
|
|
// Return 202 Accepted for async processing
|
|
return ctx.JSON(http.StatusAccepted, response)
|
|
}
|
|
|
|
// GetDocumentBatch returns batch upload status
|
|
func (s *Controllers) GetDocumentBatch(ctx echo.Context, clientId ClientID, batchId BatchID) error {
|
|
// BatchID is already a UUID type
|
|
batchUUID := uuid.UUID(batchId)
|
|
|
|
// Get batch details
|
|
batch, err := s.svc.DocumentBatch.Get(ctx.Request().Context(), clientId, batchUUID)
|
|
if err != nil {
|
|
slog.Error("unable to get batch", "error", err, "batchId", batchId)
|
|
return echo.NewHTTPError(http.StatusNotFound, "batch not found")
|
|
}
|
|
|
|
// Convert to API response format
|
|
details := BatchUploadDetails{
|
|
BatchId: BatchID(batch.ID),
|
|
ClientId: ClientID(batch.ClientID),
|
|
OriginalFilename: batch.OriginalFilename,
|
|
Status: BatchStatus(batch.Status),
|
|
TotalDocuments: batch.TotalDocuments,
|
|
ProcessedDocuments: batch.ProcessedDocuments,
|
|
FailedDocuments: batch.FailedDocuments,
|
|
InvalidTypeDocuments: batch.InvalidTypeDocuments,
|
|
ProgressPercent: batch.ProgressPercent,
|
|
FailedFilenames: &batch.FailedFilenames,
|
|
CreatedAt: batch.CreatedAt,
|
|
}
|
|
|
|
// Handle nullable completed at
|
|
if batch.CompletedAt != nil {
|
|
details.CompletedAt = nullable.NewNullableWithValue(*batch.CompletedAt)
|
|
}
|
|
|
|
return ctx.JSON(http.StatusOK, details)
|
|
}
|
|
|
|
// CancelDocumentBatch cancels a batch upload in progress
|
|
func (s *Controllers) CancelDocumentBatch(ctx echo.Context, clientId ClientID, batchId BatchID) error {
|
|
// BatchID is already a UUID type
|
|
batchUUID := uuid.UUID(batchId)
|
|
|
|
// Cancel the batch
|
|
err := s.svc.DocumentBatch.Cancel(ctx.Request().Context(), clientId, batchUUID)
|
|
if err != nil {
|
|
slog.Error("unable to cancel batch", "error", err, "batchId", batchId)
|
|
return echo.NewHTTPError(http.StatusNotFound, "batch not found or cannot be canceled")
|
|
}
|
|
|
|
// Return 204 No Content for successful cancellation
|
|
return ctx.NoContent(http.StatusNoContent)
|
|
}
|