From 8c218f162b1496f435d4208cdc9478910c750ca1 Mon Sep 17 00:00:00 2001 From: Jay Brown Date: Thu, 11 Dec 2025 14:25:51 +0000 Subject: [PATCH] Merged in feature/textExtractionsPart3 (pull request #195) enrich doc responses * enrich doc responses --- api/queryAPI/api.gen.go | 520 ++++++++++-------- api/queryAPI/documents.go | 64 ++- api/queryAPI/documents_test.go | 198 ++++++- api/queryAPI/folders.go | 59 +- api/queryAPI/folders_test.go | 8 +- internal/database/queries/document.sql | 12 + .../database/queries/fieldextractions.sql | 15 + internal/database/queries/folders.sql | 12 + internal/database/repository/document.sql.go | 46 ++ .../repository/fieldextractions.sql.go | 59 ++ internal/database/repository/folders.sql.go | 57 ++ internal/document/get.go | 111 ++++ internal/document/get_test.go | 140 +++++ internal/document/service.go | 42 ++ internal/folder/service.go | 170 ++++++ internal/folder/service_test.go | 199 +++++++ mocks/queryapi/mock_ClientInterface.go | 66 +-- .../mock_ClientWithResponsesInterface.go | 66 +-- pkg/queryAPI/api.gen.go | 149 ++++- serviceAPIs/queryAPI.yaml | 104 +++- test/process_test.go | 59 +- test/text_extraction_integration_test.go | 4 +- 22 files changed, 1797 insertions(+), 363 deletions(-) diff --git a/api/queryAPI/api.gen.go b/api/queryAPI/api.gen.go index dd4b018b..52baf979 100644 --- a/api/queryAPI/api.gen.go +++ b/api/queryAPI/api.gen.go @@ -576,24 +576,69 @@ type DocClient struct { Name ClientName `json:"name"` } -// Document The document properties. -type Document struct { +// DocumentEnriched Enriched document details with additional metadata +type DocumentEnriched struct { // ClientId The client external id ClientId ClientID `json:"client_id"` // Fields The fields and the value for the document Fields map[string]interface{} `json:"fields"` + // Filename Original filename of the document + Filename *string `json:"filename,omitempty"` + + // FolderId Parent folder ID, null if document is not in a folder + FolderId *openapi_types.UUID `json:"folderId,omitempty"` + + // HasTextRecord Whether a text extraction (field extraction) record exists for this document + HasTextRecord bool `json:"hasTextRecord"` + // Hash The document hash Hash Hash `json:"hash"` // Id The document id. Id DocumentID `json:"id"` + + // Labels All labels applied to this document + Labels []LabelRecord `json:"labels"` + + // OriginalPath Original path provided during upload (immutable) + OriginalPath *string `json:"originalPath,omitempty"` + + // TextRecord Field extraction with version information + TextRecord *FieldExtractionResponse `json:"textRecord,omitempty"` } // DocumentID The document id. type DocumentID = openapi_types.UUID +// DocumentInFolder Document within a folder with enriched metadata (lighter format, excludes client_id and fields) +type DocumentInFolder struct { + // Filename Original filename of the document + Filename *string `json:"filename,omitempty"` + + // FolderId Parent folder ID + FolderId *openapi_types.UUID `json:"folderId,omitempty"` + + // HasTextRecord Whether a text extraction record exists for this document + HasTextRecord bool `json:"hasTextRecord"` + + // Hash The document hash + Hash Hash `json:"hash"` + + // Id The document id. + Id DocumentID `json:"id"` + + // Labels All labels applied to this document + Labels []LabelRecord `json:"labels"` + + // OriginalPath Original path provided during upload (immutable) + OriginalPath *string `json:"originalPath,omitempty"` + + // TextRecord Field extraction with version information + TextRecord *FieldExtractionResponse `json:"textRecord,omitempty"` +} + // DocumentSummary The document summary properties. type DocumentSummary struct { // Hash The document hash @@ -1060,6 +1105,12 @@ type ListClientFoldersParams struct { Metrics *bool `form:"metrics,omitempty" json:"metrics,omitempty"` } +// GetDocumentParams defines parameters for GetDocument. +type GetDocumentParams struct { + // TextRecord When true, includes the full text extraction record in the response + TextRecord *bool `form:"textRecord,omitempty" json:"textRecord,omitempty"` +} + // GetCurrentFieldExtractionParams defines parameters for GetCurrentFieldExtraction. type GetCurrentFieldExtractionParams struct { // DocumentId The document ID @@ -1081,6 +1132,12 @@ type GetFieldExtractionByVersionParams struct { Version int64 `form:"version" json:"version"` } +// GetFolderDocumentsParams defines parameters for GetFolderDocuments. +type GetFolderDocumentsParams struct { + // TextRecord When true, includes the full text extraction record for each document + TextRecord *bool `form:"textRecord,omitempty" json:"textRecord,omitempty"` +} + // GetDocumentsByLabelParams defines parameters for GetDocumentsByLabel. type GetDocumentsByLabelParams struct { // ClientId The client ID to filter documents @@ -1214,7 +1271,7 @@ type ServerInterface interface { ListClients(ctx echo.Context) error // Get document details by its id // (GET /document/{id}) - GetDocument(ctx echo.Context, id DocumentID) error + GetDocument(ctx echo.Context, id DocumentID, params GetDocumentParams) error // Get all labels for a document // (GET /documents/{documentId}/labels) GetDocumentLabels(ctx echo.Context, documentId openapi_types.UUID) error @@ -1244,7 +1301,7 @@ type ServerInterface interface { RenameFolder(ctx echo.Context, folderId openapi_types.UUID) error // Get documents in a folder // (GET /folders/{folderId}/documents) - GetFolderDocuments(ctx echo.Context, folderId openapi_types.UUID) error + GetFolderDocuments(ctx echo.Context, folderId openapi_types.UUID, params GetFolderDocumentsParams) error // Get folder processing metrics // (GET /folders/{folderId}/metrics) GetFolderMetrics(ctx echo.Context, folderId openapi_types.UUID) error @@ -1778,8 +1835,17 @@ func (w *ServerInterfaceWrapper) GetDocument(ctx echo.Context) error { ctx.Set(JwtAuthScopes, []string{}) + // Parameter object where we will unmarshal all parameters from the context + var params GetDocumentParams + // ------------- Optional query parameter "textRecord" ------------- + + err = runtime.BindQueryParameter("form", true, false, "textRecord", ctx.QueryParams(), ¶ms.TextRecord) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter textRecord: %s", err)) + } + // Invoke the callback with all the unmarshaled arguments - err = w.Handler.GetDocument(ctx, id) + err = w.Handler.GetDocument(ctx, id, params) return err } @@ -1957,8 +2023,17 @@ func (w *ServerInterfaceWrapper) GetFolderDocuments(ctx echo.Context) error { ctx.Set(JwtAuthScopes, []string{}) + // Parameter object where we will unmarshal all parameters from the context + var params GetFolderDocumentsParams + // ------------- Optional query parameter "textRecord" ------------- + + err = runtime.BindQueryParameter("form", true, false, "textRecord", ctx.QueryParams(), ¶ms.TextRecord) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter textRecord: %s", err)) + } + // Invoke the callback with all the unmarshaled arguments - err = w.Handler.GetFolderDocuments(ctx, folderId) + err = w.Handler.GetFolderDocuments(ctx, folderId, params) return err } @@ -2220,219 +2295,226 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/queryAPI/documents.go b/api/queryAPI/documents.go index deda8a69..a66e5e1a 100644 --- a/api/queryAPI/documents.go +++ b/api/queryAPI/documents.go @@ -5,12 +5,14 @@ import ( "log/slog" "net/http" + "queryorchestration/internal/database/repository" "queryorchestration/internal/document/batch/storage" documentupload "queryorchestration/internal/document/upload" "github.com/google/uuid" "github.com/labstack/echo/v4" "github.com/oapi-codegen/nullable" + openapi_types "github.com/oapi-codegen/runtime/types" ) func (s *Controllers) UploadDocument(ctx echo.Context, clientId ClientID) error { @@ -64,18 +66,62 @@ func (s *Controllers) ListDocumentsByClientId(ctx echo.Context, clientId ClientI 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) +// GetDocument retrieves document details by ID with enriched metadata. +// When textRecord=true, includes the full field extraction response. +func (s *Controllers) GetDocument(ctx echo.Context, id DocumentID, params GetDocumentParams) error { + includeTextRecord := params.TextRecord != nil && *params.TextRecord + + document, err := s.svc.Document.GetEnriched(ctx.Request().Context(), uuid.UUID(id), includeTextRecord) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unable to list documents: %s", err)) + slog.Error("unable to get document", "error", err, "document_id", id) + return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Unable to get document: %s", err)) } - return ctx.JSON(http.StatusOK, Document{ - Id: document.Id, - ClientId: document.ClientID, - Hash: document.Hash, - Fields: document.Fields, - }) + // Convert labels to API format + labels := make([]LabelRecord, len(document.Labels)) + for i, lbl := range document.Labels { + labels[i] = LabelRecord{ + Id: openapi_types.UUID(lbl.ID), + DocumentId: DocumentID(lbl.DocumentID), + Label: lbl.Label, + AppliedBy: openapi_types.Email(lbl.AppliedBy), + AppliedAt: lbl.AppliedAt, + } + } + + response := DocumentEnriched{ + Id: DocumentID(document.ID), + ClientId: ClientID(document.ClientID), + Hash: Hash(document.Hash), + Fields: document.Fields, + HasTextRecord: document.HasTextRecord, + Labels: labels, + } + + if document.FolderID != nil { + response.FolderId = (*openapi_types.UUID)(document.FolderID) + } + if document.Filename != nil { + response.Filename = document.Filename + } + if document.OriginalPath != nil { + response.OriginalPath = document.OriginalPath + } + + // Include text record if requested and available + if document.TextRecord != nil { + // The TextRecord contains repository types that we need to convert + current, ok := document.TextRecord.SingleFields.(*repository.Currentfieldextraction) + if ok && current != nil { + arrayFields, ok := document.TextRecord.ArrayFields.([]*repository.Documentfieldextractionarrayfield) + if ok { + textRecord := buildFieldExtractionResponse(current, arrayFields) + response.TextRecord = textRecord + } + } + } + + return ctx.JSON(http.StatusOK, response) } // ListDocumentBatches lists batch uploads for a client diff --git a/api/queryAPI/documents_test.go b/api/queryAPI/documents_test.go index fcd0450e..f5941a98 100644 --- a/api/queryAPI/documents_test.go +++ b/api/queryAPI/documents_test.go @@ -21,6 +21,7 @@ import ( queryapi "queryorchestration/api/queryAPI" "queryorchestration/internal/database/repository" + "queryorchestration/internal/fieldextraction" "queryorchestration/internal/test" ) @@ -109,17 +110,202 @@ func TestGetDocument(t *testing.T) { ctx, rec := createContext(t) - err = cons.GetDocument(ctx, docId) + err = cons.GetDocument(ctx, docId, queryapi.GetDocumentParams{}) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) - assertBody(t, rec, queryapi.Document{ - Id: docId, - ClientId: "client_id", - Hash: "hash", - Fields: map[string]interface{}{}, + assertBody(t, rec, queryapi.DocumentEnriched{ + Id: docId, + ClientId: "client_id", + Hash: "hash", + Fields: map[string]interface{}{}, + HasTextRecord: false, + Labels: []queryapi.LabelRecord{}, }) } +func TestGetDocumentWithEnrichedFields(t *testing.T) { + cfg := &ControllerConfig{} + test.CreateDB(t, cfg) + initializeTestConfig(t, cfg) + + svc := createControllerServices(cfg) + cons := queryapi.NewControllers(svc, cfg) + + // Create test client with root folder + test.CreateTestClient(t, cfg, "enriched_client", "Enriched Client") + + // Create folder + folder, err := cfg.GetDBQueries().CreateFolder(t.Context(), &repository.CreateFolderParams{ + Path: "/documents", + Clientid: "enriched_client", + Createdby: "testuser", + }) + require.NoError(t, err) + + // Create document with folder, filename, originalPath + filename := "test-document.pdf" + originalPath := "/uploads/test-document.pdf" + docId, err := cfg.GetDBQueries().CreateDocument(t.Context(), &repository.CreateDocumentParams{ + Clientid: "enriched_client", + Hash: "enriched_hash", + Filename: &filename, + Originalpath: &originalPath, + Folderid: &folder.ID, + }) + require.NoError(t, err) + + // Add labels to document - use a label that exists in the lookup table + _, err = cfg.GetDBQueries().ApplyLabel(t.Context(), &repository.ApplyLabelParams{ + Documentid: docId, + Label: "Ingested", // This label is seeded in migration + Appliedby: "testuser@example.com", + }) + require.NoError(t, err) + + ctx, rec := createContext(t) + + // Test without textRecord parameter + err = cons.GetDocument(ctx, docId, queryapi.GetDocumentParams{}) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Code) + + var result queryapi.DocumentEnriched + err = json.Unmarshal(rec.Body.Bytes(), &result) + require.NoError(t, err) + + // Verify enriched fields + assert.Equal(t, docId, result.Id) + assert.Equal(t, queryapi.ClientID("enriched_client"), result.ClientId) + assert.Equal(t, queryapi.Hash("enriched_hash"), result.Hash) + assert.NotNil(t, result.Filename) + assert.Equal(t, filename, *result.Filename) + assert.NotNil(t, result.OriginalPath) + assert.Equal(t, originalPath, *result.OriginalPath) + assert.NotNil(t, result.FolderId) + assert.Equal(t, folder.ID.String(), result.FolderId.String()) + assert.False(t, result.HasTextRecord) + assert.Nil(t, result.TextRecord) + + // Verify labels + assert.Len(t, result.Labels, 1) + assert.Equal(t, "Ingested", result.Labels[0].Label) + assert.Equal(t, "testuser@example.com", string(result.Labels[0].AppliedBy)) +} + +func TestGetDocumentWithTextRecord(t *testing.T) { + cfg := &ControllerConfig{} + test.CreateDB(t, cfg) + initializeTestConfig(t, cfg) + + svc := createControllerServices(cfg) + cons := queryapi.NewControllers(svc, cfg) + + // Create test client with root folder + test.CreateTestClient(t, cfg, "textrecord_client", "TextRecord Client") + + // Create document + filename := "contract.pdf" + docId, err := cfg.GetDBQueries().CreateDocument(t.Context(), &repository.CreateDocumentParams{ + Clientid: "textrecord_client", + Hash: "textrecord_hash", + Filename: &filename, + }) + require.NoError(t, err) + + // Create field extraction using the fieldextraction service (which properly inserts into versions table) + fieldSvc := fieldextraction.New(cfg) + contractTitle := "Test Contract Title" + extractionUser := "extraction@example.com" + input := &fieldextraction.CreateFieldExtractionInput{ + DocumentID: docId, + SingleFields: &repository.AddFieldExtractionParams{ + Documentid: docId, + Filename: &filename, + Contracttitle: &contractTitle, + Createdby: extractionUser, + }, + ArrayFields: []*repository.AddFieldExtractionArrayFieldParams{}, + } + _, err = fieldSvc.CreateFieldExtraction(t.Context(), input) + require.NoError(t, err) + + // Test with textRecord=true parameter + includeTextRecord := true + ctx, rec := createContext(t) + + err = cons.GetDocument(ctx, docId, queryapi.GetDocumentParams{ + TextRecord: &includeTextRecord, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Code) + + var result queryapi.DocumentEnriched + err = json.Unmarshal(rec.Body.Bytes(), &result) + require.NoError(t, err) + + // Verify document has text record + assert.Equal(t, docId, result.Id) + assert.True(t, result.HasTextRecord) + assert.NotNil(t, result.TextRecord) + assert.Equal(t, int32(1), result.TextRecord.Version) + assert.Equal(t, extractionUser, string(result.TextRecord.CreatedBy)) + assert.NotNil(t, result.TextRecord.SingleFields.ContractTitle) + assert.Equal(t, contractTitle, *result.TextRecord.SingleFields.ContractTitle) +} + +func TestGetDocumentWithTextRecordNotRequested(t *testing.T) { + cfg := &ControllerConfig{} + test.CreateDB(t, cfg) + initializeTestConfig(t, cfg) + + svc := createControllerServices(cfg) + cons := queryapi.NewControllers(svc, cfg) + + // Create test client with root folder + test.CreateTestClient(t, cfg, "notext_client", "NoText Client") + + // Create document + filename := "contract2.pdf" + docId, err := cfg.GetDBQueries().CreateDocument(t.Context(), &repository.CreateDocumentParams{ + Clientid: "notext_client", + Hash: "notext_hash", + Filename: &filename, + }) + require.NoError(t, err) + + // Create field extraction using the fieldextraction service + fieldSvc := fieldextraction.New(cfg) + contractTitle := "Another Contract" + input := &fieldextraction.CreateFieldExtractionInput{ + DocumentID: docId, + SingleFields: &repository.AddFieldExtractionParams{ + Documentid: docId, + Filename: &filename, + Contracttitle: &contractTitle, + Createdby: "extraction2@example.com", + }, + ArrayFields: []*repository.AddFieldExtractionArrayFieldParams{}, + } + _, err = fieldSvc.CreateFieldExtraction(t.Context(), input) + require.NoError(t, err) + + // Test without textRecord parameter (should still have hasTextRecord=true but no textRecord body) + ctx, rec := createContext(t) + + err = cons.GetDocument(ctx, docId, queryapi.GetDocumentParams{}) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Code) + + var result queryapi.DocumentEnriched + err = json.Unmarshal(rec.Body.Bytes(), &result) + require.NoError(t, err) + + // Verify document indicates it has text record but doesn't include the full body + assert.Equal(t, docId, result.Id) + assert.True(t, result.HasTextRecord) + assert.Nil(t, result.TextRecord) // Not requested, so should be nil +} + func TestListDocumentBatches(t *testing.T) { cfg := &ControllerConfig{} test.CreateDB(t, cfg) diff --git a/api/queryAPI/folders.go b/api/queryAPI/folders.go index 813363c4..54d6ff24 100644 --- a/api/queryAPI/folders.go +++ b/api/queryAPI/folders.go @@ -70,21 +70,64 @@ func (s *Controllers) RenameFolder(ctx echo.Context, folderId openapi_types.UUID return ctx.NoContent(http.StatusNoContent) } -// GetFolderDocuments retrieves all documents in a specific folder. +// GetFolderDocuments retrieves all documents in a specific folder with enriched metadata. // GET /folders/{folderId}/documents -// Returns a list of documents in the specified folder. -func (s *Controllers) GetFolderDocuments(ctx echo.Context, folderId openapi_types.UUID) error { - documents, err := s.svc.Folder.GetDocumentsByFolder(ctx.Request().Context(), uuid.UUID(folderId)) +// 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([]DocumentSummary, len(documents)) + apiDocs := make([]DocumentInFolder, len(documents)) for i, doc := range documents { - apiDocs[i] = DocumentSummary{ - Hash: doc.Hash, - Id: openapi_types.UUID(doc.ID), + // 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 + } + + // 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 + } + } } } diff --git a/api/queryAPI/folders_test.go b/api/queryAPI/folders_test.go index d299e6c1..536934c5 100644 --- a/api/queryAPI/folders_test.go +++ b/api/queryAPI/folders_test.go @@ -302,12 +302,12 @@ func TestGetFolderDocuments_Success(t *testing.T) { rec := httptest.NewRecorder() c := e.NewContext(req, rec) - err = ctrl.GetFolderDocuments(c, openapi_types.UUID(folderRec.ID)) + err = ctrl.GetFolderDocuments(c, openapi_types.UUID(folderRec.ID), queryapi.GetFolderDocumentsParams{}) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var resp struct { - Documents []queryapi.DocumentSummary `json:"documents"` + Documents []queryapi.DocumentInFolder `json:"documents"` } err = json.Unmarshal(rec.Body.Bytes(), &resp) require.NoError(t, err) @@ -340,12 +340,12 @@ func TestGetFolderDocuments_EmptyFolder(t *testing.T) { rec := httptest.NewRecorder() c := e.NewContext(req, rec) - err = ctrl.GetFolderDocuments(c, openapi_types.UUID(folderRec.ID)) + err = ctrl.GetFolderDocuments(c, openapi_types.UUID(folderRec.ID), queryapi.GetFolderDocumentsParams{}) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var resp struct { - Documents []queryapi.DocumentSummary `json:"documents"` + Documents []queryapi.DocumentInFolder `json:"documents"` } err = json.Unmarshal(rec.Body.Bytes(), &resp) require.NoError(t, err) diff --git a/internal/database/queries/document.sql b/internal/database/queries/document.sql index 61df7f3f..9b0311c1 100644 --- a/internal/database/queries/document.sql +++ b/internal/database/queries/document.sql @@ -76,3 +76,15 @@ SELECT FROM parts p RIGHT JOIN max_part mp ON p.part = mp.max_part_num GROUP BY p.part; + +-- name: GetDocumentEnriched :one +-- Retrieves a document with extended metadata including folder and filename +SELECT + d.id, + d.clientId, + d.hash, + d.folderId, + d.filename, + d.originalPath +FROM documents d +WHERE d.id = $1; diff --git a/internal/database/queries/fieldextractions.sql b/internal/database/queries/fieldextractions.sql index b1151cec..24d2f6c8 100644 --- a/internal/database/queries/fieldextractions.sql +++ b/internal/database/queries/fieldextractions.sql @@ -250,3 +250,18 @@ WHERE id = $1; SELECT * FROM documentFieldExtractions WHERE documentId = $1 ORDER BY createdAt DESC; + +-- name: HasFieldExtraction :one +-- Checks if a field extraction exists for a document +SELECT EXISTS( + SELECT 1 FROM documentFieldExtractionVersions + WHERE documentId = $1 +) as has_extraction; + +-- name: HasFieldExtractionBatch :many +-- Checks if field extractions exist for multiple documents (for folder listing) +-- Returns one row per input document ID indicating whether extraction exists +SELECT + t.documentId, + EXISTS(SELECT 1 FROM documentFieldExtractionVersions dfev WHERE dfev.documentId = t.documentId) as has_extraction +FROM unnest(@documentIds::uuid[]) as t(documentId); diff --git a/internal/database/queries/folders.sql b/internal/database/queries/folders.sql index fd292aff..de96b6ac 100644 --- a/internal/database/queries/folders.sql +++ b/internal/database/queries/folders.sql @@ -87,3 +87,15 @@ INSERT INTO folders (path, parentId, clientId, createdBy) VALUES ($1, $2, $3, $4) ON CONFLICT (clientId, path) DO UPDATE SET path = EXCLUDED.path RETURNING *; + +-- name: GetDocumentsByFolderEnriched :many +-- Gets documents in a folder with all enriched metadata fields +SELECT + d.id, + d.hash, + d.folderId, + d.filename, + d.originalPath +FROM documents d +WHERE d.folderId = $1 +ORDER BY d.id; diff --git a/internal/database/repository/document.sql.go b/internal/database/repository/document.sql.go index 579f2e26..a9d02d0e 100644 --- a/internal/database/repository/document.sql.go +++ b/internal/database/repository/document.sql.go @@ -94,6 +94,52 @@ func (q *Queries) CreateDocument(ctx context.Context, arg *CreateDocumentParams) return id, err } +const getDocumentEnriched = `-- name: GetDocumentEnriched :one +SELECT + d.id, + d.clientId, + d.hash, + d.folderId, + d.filename, + d.originalPath +FROM documents d +WHERE d.id = $1 +` + +type GetDocumentEnrichedRow struct { + ID uuid.UUID `db:"id"` + Clientid string `db:"clientid"` + Hash string `db:"hash"` + Folderid *uuid.UUID `db:"folderid"` + Filename *string `db:"filename"` + Originalpath *string `db:"originalpath"` +} + +// Retrieves a document with extended metadata including folder and filename +// +// SELECT +// d.id, +// d.clientId, +// d.hash, +// d.folderId, +// d.filename, +// d.originalPath +// FROM documents d +// WHERE d.id = $1 +func (q *Queries) GetDocumentEnriched(ctx context.Context, id uuid.UUID) (*GetDocumentEnrichedRow, error) { + row := q.db.QueryRow(ctx, getDocumentEnriched, id) + var i GetDocumentEnrichedRow + err := row.Scan( + &i.ID, + &i.Clientid, + &i.Hash, + &i.Folderid, + &i.Filename, + &i.Originalpath, + ) + return &i, err +} + const getDocumentEntry = `-- name: GetDocumentEntry :one SELECT documentId, bucket, key FROM documentEntries WHERE documentId = $1 ORDER BY id DESC LIMIT 1 ` diff --git a/internal/database/repository/fieldextractions.sql.go b/internal/database/repository/fieldextractions.sql.go index 4f337dfa..dde77597 100644 --- a/internal/database/repository/fieldextractions.sql.go +++ b/internal/database/repository/fieldextractions.sql.go @@ -1196,6 +1196,65 @@ func (q *Queries) GetMaxFieldExtractionVersion(ctx context.Context, documentid u return max_version, err } +const hasFieldExtraction = `-- name: HasFieldExtraction :one +SELECT EXISTS( + SELECT 1 FROM documentFieldExtractionVersions + WHERE documentId = $1 +) as has_extraction +` + +// Checks if a field extraction exists for a document +// +// SELECT EXISTS( +// SELECT 1 FROM documentFieldExtractionVersions +// WHERE documentId = $1 +// ) as has_extraction +func (q *Queries) HasFieldExtraction(ctx context.Context, documentid uuid.UUID) (bool, error) { + row := q.db.QueryRow(ctx, hasFieldExtraction, documentid) + var has_extraction bool + err := row.Scan(&has_extraction) + return has_extraction, err +} + +const hasFieldExtractionBatch = `-- name: HasFieldExtractionBatch :many +SELECT + t.documentId, + EXISTS(SELECT 1 FROM documentFieldExtractionVersions dfev WHERE dfev.documentId = t.documentId) as has_extraction +FROM unnest($1::uuid[]) as t(documentId) +` + +type HasFieldExtractionBatchRow struct { + Documentid *uuid.UUID `db:"documentid"` + HasExtraction bool `db:"has_extraction"` +} + +// Checks if field extractions exist for multiple documents (for folder listing) +// Returns one row per input document ID indicating whether extraction exists +// +// SELECT +// t.documentId, +// EXISTS(SELECT 1 FROM documentFieldExtractionVersions dfev WHERE dfev.documentId = t.documentId) as has_extraction +// FROM unnest($1::uuid[]) as t(documentId) +func (q *Queries) HasFieldExtractionBatch(ctx context.Context, documentids []uuid.UUID) ([]*HasFieldExtractionBatchRow, error) { + rows, err := q.db.Query(ctx, hasFieldExtractionBatch, documentids) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*HasFieldExtractionBatchRow{} + for rows.Next() { + var i HasFieldExtractionBatchRow + if err := rows.Scan(&i.Documentid, &i.HasExtraction); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const lockFieldExtractionVersionsForDocument = `-- name: LockFieldExtractionVersionsForDocument :many SELECT id, version FROM documentFieldExtractionVersions WHERE documentId = $1 diff --git a/internal/database/repository/folders.sql.go b/internal/database/repository/folders.sql.go index f264cedb..ada1e085 100644 --- a/internal/database/repository/folders.sql.go +++ b/internal/database/repository/folders.sql.go @@ -127,6 +127,63 @@ func (q *Queries) GetDocumentsByFolder(ctx context.Context, folderid *uuid.UUID) return items, nil } +const getDocumentsByFolderEnriched = `-- name: GetDocumentsByFolderEnriched :many +SELECT + d.id, + d.hash, + d.folderId, + d.filename, + d.originalPath +FROM documents d +WHERE d.folderId = $1 +ORDER BY d.id +` + +type GetDocumentsByFolderEnrichedRow struct { + ID uuid.UUID `db:"id"` + Hash string `db:"hash"` + Folderid *uuid.UUID `db:"folderid"` + Filename *string `db:"filename"` + Originalpath *string `db:"originalpath"` +} + +// Gets documents in a folder with all enriched metadata fields +// +// SELECT +// d.id, +// d.hash, +// d.folderId, +// d.filename, +// d.originalPath +// FROM documents d +// WHERE d.folderId = $1 +// ORDER BY d.id +func (q *Queries) GetDocumentsByFolderEnriched(ctx context.Context, folderid *uuid.UUID) ([]*GetDocumentsByFolderEnrichedRow, error) { + rows, err := q.db.Query(ctx, getDocumentsByFolderEnriched, folderid) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*GetDocumentsByFolderEnrichedRow{} + for rows.Next() { + var i GetDocumentsByFolderEnrichedRow + if err := rows.Scan( + &i.ID, + &i.Hash, + &i.Folderid, + &i.Filename, + &i.Originalpath, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getFolderByID = `-- name: GetFolderByID :one SELECT id, path, parentid, clientid, createdat, createdby FROM folders WHERE id = $1 diff --git a/internal/document/get.go b/internal/document/get.go index c272d421..844e0bf4 100644 --- a/internal/document/get.go +++ b/internal/document/get.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" ) +// GetSummary returns minimal document identification info. func (s *Service) GetSummary(ctx context.Context, id uuid.UUID) (*DocumentSummary, error) { doc, err := s.cfg.GetDBQueries().GetDocumentSummary(ctx, id) if err != nil { @@ -20,6 +21,7 @@ func (s *Service) GetSummary(ctx context.Context, id uuid.UUID) (*DocumentSummar }, nil } +// GetExternal returns document info with computed fields for external API responses. func (s *Service) GetExternal(ctx context.Context, id uuid.UUID) (*DocumentExternal, error) { doc, err := s.cfg.GetDBQueries().GetDocumentExternal(ctx, id) if err != nil { @@ -39,3 +41,112 @@ func (s *Service) GetExternal(ctx context.Context, id uuid.UUID) (*DocumentExter Fields: fields, }, nil } + +// GetEnriched returns full document details including metadata, labels, and optionally the text record. +// Parameters: +// - ctx: context for the operation +// - id: document UUID +// - includeTextRecord: if true, fetches and includes the full field extraction response +// +// Returns DocumentEnriched with all metadata, or error if document not found or other failure. +func (s *Service) GetEnriched(ctx context.Context, id uuid.UUID, includeTextRecord bool) (*DocumentEnriched, error) { + queries := s.cfg.GetDBQueries() + + // 1. Get basic document info + computed fields + docExternal, err := queries.GetDocumentExternal(ctx, id) + if err != nil { + return nil, err + } + + var fields map[string]interface{} + err = json.Unmarshal(docExternal.Fields, &fields) + if err != nil { + return nil, err + } + + // 2. Get extended document metadata (folder, filename, originalPath) + docEnriched, err := queries.GetDocumentEnriched(ctx, id) + if err != nil { + return nil, err + } + + // 3. Check if text record exists + hasTextRecord, err := queries.HasFieldExtraction(ctx, id) + if err != nil { + return nil, err + } + + // 4. Get labels for document + dbLabels, err := queries.GetDocumentLabels(ctx, id) + if err != nil { + return nil, err + } + + labels := make([]LabelRecord, len(dbLabels)) + for i, lbl := range dbLabels { + labels[i] = LabelRecord{ + ID: lbl.ID, + DocumentID: lbl.Documentid, + Label: lbl.Label, + AppliedBy: lbl.Appliedby, + AppliedAt: lbl.Appliedat.Time, + } + } + + result := &DocumentEnriched{ + ID: docExternal.ID, + ClientID: docExternal.Clientid, + Hash: docExternal.Hash, + Fields: fields, + HasTextRecord: hasTextRecord, + FolderID: docEnriched.Folderid, + Filename: docEnriched.Filename, + OriginalPath: docEnriched.Originalpath, + Labels: labels, + } + + // 5. Optionally fetch full text record if requested and exists + if includeTextRecord && hasTextRecord { + textRecord, err := s.getTextRecord(ctx, id) + if err != nil { + // Log but don't fail - text record is optional enhancement + // Could add logging here if needed + return result, nil + } + result.TextRecord = textRecord + } + + return result, nil +} + +// getTextRecord fetches the current field extraction for a document. +// Returns nil if no extraction exists. +func (s *Service) getTextRecord(ctx context.Context, documentID uuid.UUID) (*TextRecord, error) { + queries := s.cfg.GetDBQueries() + + current, err := queries.GetCurrentFieldExtraction(ctx, documentID) + if err != nil { + return nil, err + } + + if current == nil { + return nil, nil + } + + // Get array fields + arrayFields, err := queries.GetFieldExtractionArrayFields(ctx, current.ID) + if err != nil { + return nil, err + } + + return &TextRecord{ + ID: current.ID, + DocumentID: current.Documentid, + //nolint:gosec // Version is a small number, int32 is sufficient + Version: int32(current.Version), + SingleFields: current, // Pass the whole struct - handler will convert + ArrayFields: arrayFields, // Pass array fields - handler will convert + CreatedAt: current.Createdat.Time, + CreatedBy: current.Createdby, + }, nil +} diff --git a/internal/document/get_test.go b/internal/document/get_test.go index 6d631670..cf8f63a9 100644 --- a/internal/document/get_test.go +++ b/internal/document/get_test.go @@ -5,7 +5,9 @@ import ( "queryorchestration/internal/database/repository" "queryorchestration/internal/document" + "queryorchestration/internal/fieldextraction" "queryorchestration/internal/serviceconfig" + "queryorchestration/internal/test" "github.com/stretchr/testify/require" @@ -14,6 +16,10 @@ import ( "github.com/stretchr/testify/assert" ) +type TestConfig struct { + serviceconfig.BaseConfig +} + func TestGetSummary(t *testing.T) { ctx := t.Context() @@ -72,3 +78,137 @@ func TestGetExternal(t *testing.T) { require.NoError(t, err) assert.Equal(t, &doc, adoc) } + +func TestGetEnriched(t *testing.T) { + ctx := t.Context() + cfg := &TestConfig{} + test.CreateDB(t, cfg) + svc := document.New(cfg) + + // Create test client + clientID := "test-client-enriched" + err := cfg.GetDBQueries().CreateClient(ctx, &repository.CreateClientParams{ + Clientid: clientID, + Name: "Test Client Enriched", + }) + require.NoError(t, err) + + t.Run("get enriched document without text record", func(t *testing.T) { + filename := "test-enriched.pdf" + originalPath := "/documents/test-enriched.pdf" + documentID, err := cfg.GetDBQueries().CreateDocument(ctx, &repository.CreateDocumentParams{ + Clientid: clientID, + Hash: "enrichedhash1", + Filename: &filename, + Originalpath: &originalPath, + }) + require.NoError(t, err) + + // Get enriched document without text record + result, err := svc.GetEnriched(ctx, documentID, false) + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, documentID, result.ID) + assert.Equal(t, clientID, result.ClientID) + assert.Equal(t, "enrichedhash1", result.Hash) + assert.False(t, result.HasTextRecord) + assert.Equal(t, filename, *result.Filename) + assert.Equal(t, originalPath, *result.OriginalPath) + assert.Nil(t, result.TextRecord) + assert.Empty(t, result.Labels) + }) + + t.Run("get enriched document with text record requested but not present", func(t *testing.T) { + filename := "test-enriched-no-text.pdf" + documentID, err := cfg.GetDBQueries().CreateDocument(ctx, &repository.CreateDocumentParams{ + Clientid: clientID, + Hash: "enrichedhash2", + Filename: &filename, + }) + require.NoError(t, err) + + // Get enriched document with text record requested + result, err := svc.GetEnriched(ctx, documentID, true) + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, documentID, result.ID) + assert.False(t, result.HasTextRecord) + assert.Nil(t, result.TextRecord) // No text record exists + }) + + t.Run("get enriched document with text record", func(t *testing.T) { + filename := "test-enriched-with-text.pdf" + documentID, err := cfg.GetDBQueries().CreateDocument(ctx, &repository.CreateDocumentParams{ + Clientid: clientID, + Hash: "enrichedhash3", + Filename: &filename, + }) + require.NoError(t, err) + + // Create field extraction for this document + fieldSvc := fieldextraction.New(cfg) + contractTitle := "Test Contract" + input := &fieldextraction.CreateFieldExtractionInput{ + DocumentID: documentID, + SingleFields: &repository.AddFieldExtractionParams{ + Documentid: documentID, + Filename: &filename, + Contracttitle: &contractTitle, + Createdby: "user123", + }, + ArrayFields: []*repository.AddFieldExtractionArrayFieldParams{}, + } + _, err = fieldSvc.CreateFieldExtraction(ctx, input) + require.NoError(t, err) + + // Get enriched document with text record + result, err := svc.GetEnriched(ctx, documentID, true) + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, documentID, result.ID) + assert.True(t, result.HasTextRecord) + assert.NotNil(t, result.TextRecord) + assert.Equal(t, documentID, result.TextRecord.DocumentID) + assert.Equal(t, int32(1), result.TextRecord.Version) + assert.Equal(t, "user123", result.TextRecord.CreatedBy) + }) + + t.Run("get enriched document with text record but not requesting it", func(t *testing.T) { + filename := "test-enriched-skip-text.pdf" + documentID, err := cfg.GetDBQueries().CreateDocument(ctx, &repository.CreateDocumentParams{ + Clientid: clientID, + Hash: "enrichedhash4", + Filename: &filename, + }) + require.NoError(t, err) + + // Create field extraction + fieldSvc := fieldextraction.New(cfg) + contractTitle := "Test Contract Skip" + input := &fieldextraction.CreateFieldExtractionInput{ + DocumentID: documentID, + SingleFields: &repository.AddFieldExtractionParams{ + Documentid: documentID, + Filename: &filename, + Contracttitle: &contractTitle, + Createdby: "user456", + }, + ArrayFields: []*repository.AddFieldExtractionArrayFieldParams{}, + } + _, err = fieldSvc.CreateFieldExtraction(ctx, input) + require.NoError(t, err) + + // Get enriched document without requesting text record + result, err := svc.GetEnriched(ctx, documentID, false) + require.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.HasTextRecord) // Has text record + assert.Nil(t, result.TextRecord) // But not fetched + }) + + t.Run("return error for non-existent document", func(t *testing.T) { + nonExistentID := uuid.New() + _, err := svc.GetEnriched(ctx, nonExistentID, false) + require.Error(t, err) + }) +} diff --git a/internal/document/service.go b/internal/document/service.go index 093a9979..bb04cda7 100644 --- a/internal/document/service.go +++ b/internal/document/service.go @@ -1,17 +1,21 @@ package document import ( + "time" + "queryorchestration/internal/serviceconfig" "github.com/google/uuid" ) +// DocumentSummary contains minimal document identification info. type DocumentSummary struct { ID uuid.UUID ClientID string Hash string } +// DocumentExternal contains document info with computed fields for external API responses. type DocumentExternal struct { Id uuid.UUID ClientID string @@ -19,10 +23,48 @@ type DocumentExternal struct { Fields map[string]interface{} } +// DocumentEnriched contains full document details including metadata, labels, and optionally text record. +// Used by GET /document/{id} endpoint. +type DocumentEnriched struct { + ID uuid.UUID + ClientID string + Hash string + Fields map[string]interface{} + HasTextRecord bool + FolderID *uuid.UUID + Filename *string + OriginalPath *string + Labels []LabelRecord + TextRecord *TextRecord // Only populated when includeTextRecord=true +} + +// LabelRecord represents a label applied to a document. +type LabelRecord struct { + ID uuid.UUID + DocumentID uuid.UUID + Label string + AppliedBy string + AppliedAt time.Time +} + +// TextRecord represents the field extraction response for a document. +// This mirrors the FieldExtractionResponse from the API. +type TextRecord struct { + ID uuid.UUID + DocumentID uuid.UUID + Version int32 + SingleFields interface{} + ArrayFields interface{} + CreatedAt time.Time + CreatedBy string +} + +// Service provides document-related operations. type Service struct { cfg serviceconfig.ConfigProvider } +// New creates a new Document service instance. func New(cfg serviceconfig.ConfigProvider) *Service { return &Service{ cfg, diff --git a/internal/folder/service.go b/internal/folder/service.go index 7eaab0be..3f41a883 100644 --- a/internal/folder/service.go +++ b/internal/folder/service.go @@ -3,6 +3,7 @@ package folder import ( "context" "fmt" + "time" "queryorchestration/internal/database/repository" "queryorchestration/internal/serviceconfig" @@ -235,3 +236,172 @@ func safeInt64ToInt32(n int64) int32 { } return int32(n) //nolint:gosec // bounds check performed above } + +// DocumentInFolder represents a document in a folder with enriched metadata. +// This is a lighter-weight format than DocumentEnriched, excluding client_id and computed fields. +type DocumentInFolder struct { + ID uuid.UUID + Hash string + HasTextRecord bool + FolderID *uuid.UUID + Filename *string + OriginalPath *string + Labels []LabelRecord + TextRecord *TextRecord // Only populated when includeTextRecord=true +} + +// LabelRecord represents a label applied to a document. +type LabelRecord struct { + ID uuid.UUID + DocumentID uuid.UUID + Label string + AppliedBy string + AppliedAt time.Time +} + +// TextRecord represents the field extraction response for a document. +type TextRecord struct { + ID uuid.UUID + DocumentID uuid.UUID + Version int32 + SingleFields interface{} + ArrayFields interface{} + CreatedAt time.Time + CreatedBy string +} + +// GetDocumentsEnriched retrieves all documents in a folder with enriched metadata. +// This returns a lighter-weight format than GET /document/{id}, excluding client_id and computed fields. +// +// Parameters: +// - ctx: request context +// - folderID: UUID of the folder +// - includeTextRecord: if true, fetches and includes the full field extraction response for each document +// +// Returns: +// - []DocumentInFolder: documents with enriched metadata +// - error: database error or nil on success +func (s *Service) GetDocumentsEnriched(ctx context.Context, folderID uuid.UUID, includeTextRecord bool) ([]DocumentInFolder, error) { + queries := s.cfg.GetDBQueries() + + // 1. Get documents with enriched metadata + docs, err := queries.GetDocumentsByFolderEnriched(ctx, &folderID) + if err != nil { + return nil, fmt.Errorf("failed to get documents by folder: %w", err) + } + + if len(docs) == 0 { + return []DocumentInFolder{}, nil + } + + // 2. Collect document IDs for batch operations + docIDs := make([]uuid.UUID, len(docs)) + for i, doc := range docs { + docIDs[i] = doc.ID + } + + // 3. Batch check for text record existence + textRecordExists, err := queries.HasFieldExtractionBatch(ctx, docIDs) + if err != nil { + return nil, fmt.Errorf("failed to check field extractions: %w", err) + } + + // Build map for quick lookup + hasTextRecordMap := make(map[uuid.UUID]bool) + for _, tr := range textRecordExists { + if tr.Documentid != nil { + hasTextRecordMap[*tr.Documentid] = tr.HasExtraction + } + } + + // 4. Get labels for all documents + // We need to fetch labels per document since there's no batch query + labelsMap := make(map[uuid.UUID][]LabelRecord) + for _, docID := range docIDs { + dbLabels, err := queries.GetDocumentLabels(ctx, docID) + if err != nil { + // Log but don't fail - labels are optional enhancement + continue + } + labels := make([]LabelRecord, len(dbLabels)) + for i, lbl := range dbLabels { + labels[i] = LabelRecord{ + ID: lbl.ID, + DocumentID: lbl.Documentid, + Label: lbl.Label, + AppliedBy: lbl.Appliedby, + AppliedAt: lbl.Appliedat.Time, + } + } + labelsMap[docID] = labels + } + + // 5. Optionally fetch text records + textRecordsMap := make(map[uuid.UUID]*TextRecord) + if includeTextRecord { + for _, docID := range docIDs { + if hasTextRecordMap[docID] { + tr, err := s.getTextRecord(ctx, docID) + if err != nil { + // Log but don't fail - text record is optional + continue + } + textRecordsMap[docID] = tr + } + } + } + + // 6. Build result + result := make([]DocumentInFolder, len(docs)) + for i, doc := range docs { + labels := labelsMap[doc.ID] + if labels == nil { + labels = []LabelRecord{} + } + + result[i] = DocumentInFolder{ + ID: doc.ID, + Hash: doc.Hash, + HasTextRecord: hasTextRecordMap[doc.ID], + FolderID: doc.Folderid, + Filename: doc.Filename, + OriginalPath: doc.Originalpath, + Labels: labels, + TextRecord: textRecordsMap[doc.ID], + } + } + + return result, nil +} + +// getTextRecord fetches the current field extraction for a document. +// Returns nil if no extraction exists. +func (s *Service) getTextRecord(ctx context.Context, documentID uuid.UUID) (*TextRecord, error) { + queries := s.cfg.GetDBQueries() + + current, err := queries.GetCurrentFieldExtraction(ctx, documentID) + if err != nil { + return nil, err + } + + if current == nil { + return nil, nil + } + + // Get array fields + arrayFields, err := queries.GetFieldExtractionArrayFields(ctx, current.ID) + if err != nil { + return nil, err + } + + return &TextRecord{ + ID: current.ID, + DocumentID: current.Documentid, + //nolint:gosec // Version is a small number, int32 is sufficient + Version: int32(current.Version), + SingleFields: current, // Pass the whole struct - handler will convert + ArrayFields: arrayFields, // Pass array fields - handler will convert + CreatedAt: current.Createdat.Time, + CreatedBy: current.Createdby, + }, nil +} diff --git a/internal/folder/service_test.go b/internal/folder/service_test.go index 4cbcf816..06982493 100644 --- a/internal/folder/service_test.go +++ b/internal/folder/service_test.go @@ -4,6 +4,7 @@ import ( "testing" "queryorchestration/internal/database/repository" + "queryorchestration/internal/fieldextraction" "queryorchestration/internal/folder" "queryorchestration/internal/serviceconfig" "queryorchestration/internal/test" @@ -329,3 +330,201 @@ func TestGetSubfolders(t *testing.T) { assert.Equal(t, child.ID, result[0].ID) assert.Equal(t, parent.ID, *result[0].Parentid) } + +func TestGetDocumentsByFolder(t *testing.T) { + ctx := t.Context() + cfg := &TestConfig{} + test.CreateDB(t, cfg) + svc := folder.New(cfg) + + clientID := "test-client-docs-by-folder" + err := cfg.GetDBQueries().CreateClient(ctx, &repository.CreateClientParams{ + Clientid: clientID, + Name: "Test Client Docs By Folder", + }) + require.NoError(t, err) + + t.Run("get documents in folder", func(t *testing.T) { + // Create folder + folderResult, err := svc.CreateFolder(ctx, "/contracts", nil, clientID, "user123") + require.NoError(t, err) + + // Create documents in folder + filename1 := "contract1.pdf" + doc1ID, err := cfg.GetDBQueries().CreateDocument(ctx, &repository.CreateDocumentParams{ + Clientid: clientID, + Hash: "foldertest1", + Filename: &filename1, + Folderid: &folderResult.ID, + }) + require.NoError(t, err) + + filename2 := "contract2.pdf" + doc2ID, err := cfg.GetDBQueries().CreateDocument(ctx, &repository.CreateDocumentParams{ + Clientid: clientID, + Hash: "foldertest2", + Filename: &filename2, + Folderid: &folderResult.ID, + }) + require.NoError(t, err) + + // Get documents by folder + docs, err := svc.GetDocumentsByFolder(ctx, folderResult.ID) + require.NoError(t, err) + assert.Len(t, docs, 2) + + // Verify both documents are returned + docIDs := make(map[uuid.UUID]bool) + for _, doc := range docs { + docIDs[doc.ID] = true + } + assert.True(t, docIDs[doc1ID]) + assert.True(t, docIDs[doc2ID]) + }) + + t.Run("return empty for folder with no documents", func(t *testing.T) { + // Create folder with no documents + emptyFolder, err := svc.CreateFolder(ctx, "/empty", nil, clientID, "user123") + require.NoError(t, err) + + docs, err := svc.GetDocumentsByFolder(ctx, emptyFolder.ID) + require.NoError(t, err) + assert.Len(t, docs, 0) + }) +} + +func TestGetDocumentsEnriched(t *testing.T) { + ctx := t.Context() + cfg := &TestConfig{} + test.CreateDB(t, cfg) + svc := folder.New(cfg) + + clientID := "test-client-docs-enriched" + err := cfg.GetDBQueries().CreateClient(ctx, &repository.CreateClientParams{ + Clientid: clientID, + Name: "Test Client Docs Enriched", + }) + require.NoError(t, err) + + t.Run("get enriched documents without text record", func(t *testing.T) { + // Create folder + folderResult, err := svc.CreateFolder(ctx, "/enriched-no-text", nil, clientID, "user123") + require.NoError(t, err) + + // Create document in folder + filename := "enriched1.pdf" + originalPath := "/uploads/enriched1.pdf" + docID, err := cfg.GetDBQueries().CreateDocument(ctx, &repository.CreateDocumentParams{ + Clientid: clientID, + Hash: "enrichedtest1", + Filename: &filename, + Originalpath: &originalPath, + Folderid: &folderResult.ID, + }) + require.NoError(t, err) + + // Get enriched documents + docs, err := svc.GetDocumentsEnriched(ctx, folderResult.ID, false) + require.NoError(t, err) + assert.Len(t, docs, 1) + assert.Equal(t, docID, docs[0].ID) + assert.Equal(t, "enrichedtest1", docs[0].Hash) + assert.Equal(t, filename, *docs[0].Filename) + assert.Equal(t, originalPath, *docs[0].OriginalPath) + assert.False(t, docs[0].HasTextRecord) + assert.Nil(t, docs[0].TextRecord) + assert.Empty(t, docs[0].Labels) + }) + + t.Run("get enriched documents with text record", func(t *testing.T) { + // Create folder + folderResult, err := svc.CreateFolder(ctx, "/enriched-with-text", nil, clientID, "user123") + require.NoError(t, err) + + // Create document in folder + filename := "enriched2.pdf" + docID, err := cfg.GetDBQueries().CreateDocument(ctx, &repository.CreateDocumentParams{ + Clientid: clientID, + Hash: "enrichedtest2", + Filename: &filename, + Folderid: &folderResult.ID, + }) + require.NoError(t, err) + + // Create field extraction for this document + fieldSvc := fieldextraction.New(cfg) + contractTitle := "Enriched Test Contract" + input := &fieldextraction.CreateFieldExtractionInput{ + DocumentID: docID, + SingleFields: &repository.AddFieldExtractionParams{ + Documentid: docID, + Filename: &filename, + Contracttitle: &contractTitle, + Createdby: "testuser", + }, + ArrayFields: []*repository.AddFieldExtractionArrayFieldParams{}, + } + _, err = fieldSvc.CreateFieldExtraction(ctx, input) + require.NoError(t, err) + + // Get enriched documents with text record + docs, err := svc.GetDocumentsEnriched(ctx, folderResult.ID, true) + require.NoError(t, err) + assert.Len(t, docs, 1) + assert.Equal(t, docID, docs[0].ID) + assert.True(t, docs[0].HasTextRecord) + assert.NotNil(t, docs[0].TextRecord) + assert.Equal(t, docID, docs[0].TextRecord.DocumentID) + assert.Equal(t, int32(1), docs[0].TextRecord.Version) + assert.Equal(t, "testuser", docs[0].TextRecord.CreatedBy) + }) + + t.Run("get enriched documents with text record but not requesting it", func(t *testing.T) { + // Create folder + folderResult, err := svc.CreateFolder(ctx, "/enriched-skip-text", nil, clientID, "user123") + require.NoError(t, err) + + // Create document in folder + filename := "enriched3.pdf" + docID, err := cfg.GetDBQueries().CreateDocument(ctx, &repository.CreateDocumentParams{ + Clientid: clientID, + Hash: "enrichedtest3", + Filename: &filename, + Folderid: &folderResult.ID, + }) + require.NoError(t, err) + + // Create field extraction + fieldSvc := fieldextraction.New(cfg) + contractTitle := "Skip Text Contract" + input := &fieldextraction.CreateFieldExtractionInput{ + DocumentID: docID, + SingleFields: &repository.AddFieldExtractionParams{ + Documentid: docID, + Filename: &filename, + Contracttitle: &contractTitle, + Createdby: "skipuser", + }, + ArrayFields: []*repository.AddFieldExtractionArrayFieldParams{}, + } + _, err = fieldSvc.CreateFieldExtraction(ctx, input) + require.NoError(t, err) + + // Get enriched documents without requesting text record + docs, err := svc.GetDocumentsEnriched(ctx, folderResult.ID, false) + require.NoError(t, err) + assert.Len(t, docs, 1) + assert.True(t, docs[0].HasTextRecord) // Has text record + assert.Nil(t, docs[0].TextRecord) // But not fetched + }) + + t.Run("return empty for empty folder", func(t *testing.T) { + // Create folder with no documents + emptyFolder, err := svc.CreateFolder(ctx, "/enriched-empty", nil, clientID, "user123") + require.NoError(t, err) + + docs, err := svc.GetDocumentsEnriched(ctx, emptyFolder.ID, false) + require.NoError(t, err) + assert.Len(t, docs, 0) + }) +} diff --git a/mocks/queryapi/mock_ClientInterface.go b/mocks/queryapi/mock_ClientInterface.go index c357ad2b..8e730880 100644 --- a/mocks/queryapi/mock_ClientInterface.go +++ b/mocks/queryapi/mock_ClientInterface.go @@ -1666,14 +1666,14 @@ func (_c *MockClientInterface_GetCurrentFieldExtraction_Call) RunAndReturn(run f return _c } -// GetDocument provides a mock function with given fields: ctx, id, reqEditors -func (_m *MockClientInterface) GetDocument(ctx context.Context, id queryapi.DocumentID, reqEditors ...queryapi.RequestEditorFn) (*http.Response, error) { +// GetDocument provides a mock function with given fields: ctx, id, params, reqEditors +func (_m *MockClientInterface) GetDocument(ctx context.Context, id queryapi.DocumentID, params *queryapi.GetDocumentParams, reqEditors ...queryapi.RequestEditorFn) (*http.Response, error) { _va := make([]interface{}, len(reqEditors)) for _i := range reqEditors { _va[_i] = reqEditors[_i] } var _ca []interface{} - _ca = append(_ca, ctx, id) + _ca = append(_ca, ctx, id, params) _ca = append(_ca, _va...) ret := _m.Called(_ca...) @@ -1683,19 +1683,19 @@ func (_m *MockClientInterface) GetDocument(ctx context.Context, id queryapi.Docu var r0 *http.Response var r1 error - if rf, ok := ret.Get(0).(func(context.Context, queryapi.DocumentID, ...queryapi.RequestEditorFn) (*http.Response, error)); ok { - return rf(ctx, id, reqEditors...) + if rf, ok := ret.Get(0).(func(context.Context, queryapi.DocumentID, *queryapi.GetDocumentParams, ...queryapi.RequestEditorFn) (*http.Response, error)); ok { + return rf(ctx, id, params, reqEditors...) } - if rf, ok := ret.Get(0).(func(context.Context, queryapi.DocumentID, ...queryapi.RequestEditorFn) *http.Response); ok { - r0 = rf(ctx, id, reqEditors...) + if rf, ok := ret.Get(0).(func(context.Context, queryapi.DocumentID, *queryapi.GetDocumentParams, ...queryapi.RequestEditorFn) *http.Response); ok { + r0 = rf(ctx, id, params, reqEditors...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*http.Response) } } - if rf, ok := ret.Get(1).(func(context.Context, queryapi.DocumentID, ...queryapi.RequestEditorFn) error); ok { - r1 = rf(ctx, id, reqEditors...) + if rf, ok := ret.Get(1).(func(context.Context, queryapi.DocumentID, *queryapi.GetDocumentParams, ...queryapi.RequestEditorFn) error); ok { + r1 = rf(ctx, id, params, reqEditors...) } else { r1 = ret.Error(1) } @@ -1711,21 +1711,22 @@ type MockClientInterface_GetDocument_Call struct { // GetDocument is a helper method to define mock.On call // - ctx context.Context // - id queryapi.DocumentID +// - params *queryapi.GetDocumentParams // - reqEditors ...queryapi.RequestEditorFn -func (_e *MockClientInterface_Expecter) GetDocument(ctx interface{}, id interface{}, reqEditors ...interface{}) *MockClientInterface_GetDocument_Call { +func (_e *MockClientInterface_Expecter) GetDocument(ctx interface{}, id interface{}, params interface{}, reqEditors ...interface{}) *MockClientInterface_GetDocument_Call { return &MockClientInterface_GetDocument_Call{Call: _e.mock.On("GetDocument", - append([]interface{}{ctx, id}, reqEditors...)...)} + append([]interface{}{ctx, id, params}, reqEditors...)...)} } -func (_c *MockClientInterface_GetDocument_Call) Run(run func(ctx context.Context, id queryapi.DocumentID, reqEditors ...queryapi.RequestEditorFn)) *MockClientInterface_GetDocument_Call { +func (_c *MockClientInterface_GetDocument_Call) Run(run func(ctx context.Context, id queryapi.DocumentID, params *queryapi.GetDocumentParams, reqEditors ...queryapi.RequestEditorFn)) *MockClientInterface_GetDocument_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]queryapi.RequestEditorFn, len(args)-2) - for i, a := range args[2:] { + variadicArgs := make([]queryapi.RequestEditorFn, len(args)-3) + for i, a := range args[3:] { if a != nil { variadicArgs[i] = a.(queryapi.RequestEditorFn) } } - run(args[0].(context.Context), args[1].(queryapi.DocumentID), variadicArgs...) + run(args[0].(context.Context), args[1].(queryapi.DocumentID), args[2].(*queryapi.GetDocumentParams), variadicArgs...) }) return _c } @@ -1735,7 +1736,7 @@ func (_c *MockClientInterface_GetDocument_Call) Return(_a0 *http.Response, _a1 e return _c } -func (_c *MockClientInterface_GetDocument_Call) RunAndReturn(run func(context.Context, queryapi.DocumentID, ...queryapi.RequestEditorFn) (*http.Response, error)) *MockClientInterface_GetDocument_Call { +func (_c *MockClientInterface_GetDocument_Call) RunAndReturn(run func(context.Context, queryapi.DocumentID, *queryapi.GetDocumentParams, ...queryapi.RequestEditorFn) (*http.Response, error)) *MockClientInterface_GetDocument_Call { _c.Call.Return(run) return _c } @@ -2112,14 +2113,14 @@ func (_c *MockClientInterface_GetFieldExtractionHistory_Call) RunAndReturn(run f return _c } -// GetFolderDocuments provides a mock function with given fields: ctx, folderId, reqEditors -func (_m *MockClientInterface) GetFolderDocuments(ctx context.Context, folderId types.UUID, reqEditors ...queryapi.RequestEditorFn) (*http.Response, error) { +// GetFolderDocuments provides a mock function with given fields: ctx, folderId, params, reqEditors +func (_m *MockClientInterface) GetFolderDocuments(ctx context.Context, folderId types.UUID, params *queryapi.GetFolderDocumentsParams, reqEditors ...queryapi.RequestEditorFn) (*http.Response, error) { _va := make([]interface{}, len(reqEditors)) for _i := range reqEditors { _va[_i] = reqEditors[_i] } var _ca []interface{} - _ca = append(_ca, ctx, folderId) + _ca = append(_ca, ctx, folderId, params) _ca = append(_ca, _va...) ret := _m.Called(_ca...) @@ -2129,19 +2130,19 @@ func (_m *MockClientInterface) GetFolderDocuments(ctx context.Context, folderId var r0 *http.Response var r1 error - if rf, ok := ret.Get(0).(func(context.Context, types.UUID, ...queryapi.RequestEditorFn) (*http.Response, error)); ok { - return rf(ctx, folderId, reqEditors...) + if rf, ok := ret.Get(0).(func(context.Context, types.UUID, *queryapi.GetFolderDocumentsParams, ...queryapi.RequestEditorFn) (*http.Response, error)); ok { + return rf(ctx, folderId, params, reqEditors...) } - if rf, ok := ret.Get(0).(func(context.Context, types.UUID, ...queryapi.RequestEditorFn) *http.Response); ok { - r0 = rf(ctx, folderId, reqEditors...) + if rf, ok := ret.Get(0).(func(context.Context, types.UUID, *queryapi.GetFolderDocumentsParams, ...queryapi.RequestEditorFn) *http.Response); ok { + r0 = rf(ctx, folderId, params, reqEditors...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*http.Response) } } - if rf, ok := ret.Get(1).(func(context.Context, types.UUID, ...queryapi.RequestEditorFn) error); ok { - r1 = rf(ctx, folderId, reqEditors...) + if rf, ok := ret.Get(1).(func(context.Context, types.UUID, *queryapi.GetFolderDocumentsParams, ...queryapi.RequestEditorFn) error); ok { + r1 = rf(ctx, folderId, params, reqEditors...) } else { r1 = ret.Error(1) } @@ -2157,21 +2158,22 @@ type MockClientInterface_GetFolderDocuments_Call struct { // GetFolderDocuments is a helper method to define mock.On call // - ctx context.Context // - folderId types.UUID +// - params *queryapi.GetFolderDocumentsParams // - reqEditors ...queryapi.RequestEditorFn -func (_e *MockClientInterface_Expecter) GetFolderDocuments(ctx interface{}, folderId interface{}, reqEditors ...interface{}) *MockClientInterface_GetFolderDocuments_Call { +func (_e *MockClientInterface_Expecter) GetFolderDocuments(ctx interface{}, folderId interface{}, params interface{}, reqEditors ...interface{}) *MockClientInterface_GetFolderDocuments_Call { return &MockClientInterface_GetFolderDocuments_Call{Call: _e.mock.On("GetFolderDocuments", - append([]interface{}{ctx, folderId}, reqEditors...)...)} + append([]interface{}{ctx, folderId, params}, reqEditors...)...)} } -func (_c *MockClientInterface_GetFolderDocuments_Call) Run(run func(ctx context.Context, folderId types.UUID, reqEditors ...queryapi.RequestEditorFn)) *MockClientInterface_GetFolderDocuments_Call { +func (_c *MockClientInterface_GetFolderDocuments_Call) Run(run func(ctx context.Context, folderId types.UUID, params *queryapi.GetFolderDocumentsParams, reqEditors ...queryapi.RequestEditorFn)) *MockClientInterface_GetFolderDocuments_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]queryapi.RequestEditorFn, len(args)-2) - for i, a := range args[2:] { + variadicArgs := make([]queryapi.RequestEditorFn, len(args)-3) + for i, a := range args[3:] { if a != nil { variadicArgs[i] = a.(queryapi.RequestEditorFn) } } - run(args[0].(context.Context), args[1].(types.UUID), variadicArgs...) + run(args[0].(context.Context), args[1].(types.UUID), args[2].(*queryapi.GetFolderDocumentsParams), variadicArgs...) }) return _c } @@ -2181,7 +2183,7 @@ func (_c *MockClientInterface_GetFolderDocuments_Call) Return(_a0 *http.Response return _c } -func (_c *MockClientInterface_GetFolderDocuments_Call) RunAndReturn(run func(context.Context, types.UUID, ...queryapi.RequestEditorFn) (*http.Response, error)) *MockClientInterface_GetFolderDocuments_Call { +func (_c *MockClientInterface_GetFolderDocuments_Call) RunAndReturn(run func(context.Context, types.UUID, *queryapi.GetFolderDocumentsParams, ...queryapi.RequestEditorFn) (*http.Response, error)) *MockClientInterface_GetFolderDocuments_Call { _c.Call.Return(run) return _c } diff --git a/mocks/queryapi/mock_ClientWithResponsesInterface.go b/mocks/queryapi/mock_ClientWithResponsesInterface.go index a209023f..dba3f292 100644 --- a/mocks/queryapi/mock_ClientWithResponsesInterface.go +++ b/mocks/queryapi/mock_ClientWithResponsesInterface.go @@ -1813,14 +1813,14 @@ func (_c *MockClientWithResponsesInterface_GetDocumentLabelsWithResponse_Call) R return _c } -// GetDocumentWithResponse provides a mock function with given fields: ctx, id, reqEditors -func (_m *MockClientWithResponsesInterface) GetDocumentWithResponse(ctx context.Context, id queryapi.DocumentID, reqEditors ...queryapi.RequestEditorFn) (*queryapi.GetDocumentResponse, error) { +// GetDocumentWithResponse provides a mock function with given fields: ctx, id, params, reqEditors +func (_m *MockClientWithResponsesInterface) GetDocumentWithResponse(ctx context.Context, id queryapi.DocumentID, params *queryapi.GetDocumentParams, reqEditors ...queryapi.RequestEditorFn) (*queryapi.GetDocumentResponse, error) { _va := make([]interface{}, len(reqEditors)) for _i := range reqEditors { _va[_i] = reqEditors[_i] } var _ca []interface{} - _ca = append(_ca, ctx, id) + _ca = append(_ca, ctx, id, params) _ca = append(_ca, _va...) ret := _m.Called(_ca...) @@ -1830,19 +1830,19 @@ func (_m *MockClientWithResponsesInterface) GetDocumentWithResponse(ctx context. var r0 *queryapi.GetDocumentResponse var r1 error - if rf, ok := ret.Get(0).(func(context.Context, queryapi.DocumentID, ...queryapi.RequestEditorFn) (*queryapi.GetDocumentResponse, error)); ok { - return rf(ctx, id, reqEditors...) + if rf, ok := ret.Get(0).(func(context.Context, queryapi.DocumentID, *queryapi.GetDocumentParams, ...queryapi.RequestEditorFn) (*queryapi.GetDocumentResponse, error)); ok { + return rf(ctx, id, params, reqEditors...) } - if rf, ok := ret.Get(0).(func(context.Context, queryapi.DocumentID, ...queryapi.RequestEditorFn) *queryapi.GetDocumentResponse); ok { - r0 = rf(ctx, id, reqEditors...) + if rf, ok := ret.Get(0).(func(context.Context, queryapi.DocumentID, *queryapi.GetDocumentParams, ...queryapi.RequestEditorFn) *queryapi.GetDocumentResponse); ok { + r0 = rf(ctx, id, params, reqEditors...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*queryapi.GetDocumentResponse) } } - if rf, ok := ret.Get(1).(func(context.Context, queryapi.DocumentID, ...queryapi.RequestEditorFn) error); ok { - r1 = rf(ctx, id, reqEditors...) + if rf, ok := ret.Get(1).(func(context.Context, queryapi.DocumentID, *queryapi.GetDocumentParams, ...queryapi.RequestEditorFn) error); ok { + r1 = rf(ctx, id, params, reqEditors...) } else { r1 = ret.Error(1) } @@ -1858,21 +1858,22 @@ type MockClientWithResponsesInterface_GetDocumentWithResponse_Call struct { // GetDocumentWithResponse is a helper method to define mock.On call // - ctx context.Context // - id queryapi.DocumentID +// - params *queryapi.GetDocumentParams // - reqEditors ...queryapi.RequestEditorFn -func (_e *MockClientWithResponsesInterface_Expecter) GetDocumentWithResponse(ctx interface{}, id interface{}, reqEditors ...interface{}) *MockClientWithResponsesInterface_GetDocumentWithResponse_Call { +func (_e *MockClientWithResponsesInterface_Expecter) GetDocumentWithResponse(ctx interface{}, id interface{}, params interface{}, reqEditors ...interface{}) *MockClientWithResponsesInterface_GetDocumentWithResponse_Call { return &MockClientWithResponsesInterface_GetDocumentWithResponse_Call{Call: _e.mock.On("GetDocumentWithResponse", - append([]interface{}{ctx, id}, reqEditors...)...)} + append([]interface{}{ctx, id, params}, reqEditors...)...)} } -func (_c *MockClientWithResponsesInterface_GetDocumentWithResponse_Call) Run(run func(ctx context.Context, id queryapi.DocumentID, reqEditors ...queryapi.RequestEditorFn)) *MockClientWithResponsesInterface_GetDocumentWithResponse_Call { +func (_c *MockClientWithResponsesInterface_GetDocumentWithResponse_Call) Run(run func(ctx context.Context, id queryapi.DocumentID, params *queryapi.GetDocumentParams, reqEditors ...queryapi.RequestEditorFn)) *MockClientWithResponsesInterface_GetDocumentWithResponse_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]queryapi.RequestEditorFn, len(args)-2) - for i, a := range args[2:] { + variadicArgs := make([]queryapi.RequestEditorFn, len(args)-3) + for i, a := range args[3:] { if a != nil { variadicArgs[i] = a.(queryapi.RequestEditorFn) } } - run(args[0].(context.Context), args[1].(queryapi.DocumentID), variadicArgs...) + run(args[0].(context.Context), args[1].(queryapi.DocumentID), args[2].(*queryapi.GetDocumentParams), variadicArgs...) }) return _c } @@ -1882,7 +1883,7 @@ func (_c *MockClientWithResponsesInterface_GetDocumentWithResponse_Call) Return( return _c } -func (_c *MockClientWithResponsesInterface_GetDocumentWithResponse_Call) RunAndReturn(run func(context.Context, queryapi.DocumentID, ...queryapi.RequestEditorFn) (*queryapi.GetDocumentResponse, error)) *MockClientWithResponsesInterface_GetDocumentWithResponse_Call { +func (_c *MockClientWithResponsesInterface_GetDocumentWithResponse_Call) RunAndReturn(run func(context.Context, queryapi.DocumentID, *queryapi.GetDocumentParams, ...queryapi.RequestEditorFn) (*queryapi.GetDocumentResponse, error)) *MockClientWithResponsesInterface_GetDocumentWithResponse_Call { _c.Call.Return(run) return _c } @@ -2110,14 +2111,14 @@ func (_c *MockClientWithResponsesInterface_GetFieldExtractionHistoryWithResponse return _c } -// GetFolderDocumentsWithResponse provides a mock function with given fields: ctx, folderId, reqEditors -func (_m *MockClientWithResponsesInterface) GetFolderDocumentsWithResponse(ctx context.Context, folderId types.UUID, reqEditors ...queryapi.RequestEditorFn) (*queryapi.GetFolderDocumentsResponse, error) { +// GetFolderDocumentsWithResponse provides a mock function with given fields: ctx, folderId, params, reqEditors +func (_m *MockClientWithResponsesInterface) GetFolderDocumentsWithResponse(ctx context.Context, folderId types.UUID, params *queryapi.GetFolderDocumentsParams, reqEditors ...queryapi.RequestEditorFn) (*queryapi.GetFolderDocumentsResponse, error) { _va := make([]interface{}, len(reqEditors)) for _i := range reqEditors { _va[_i] = reqEditors[_i] } var _ca []interface{} - _ca = append(_ca, ctx, folderId) + _ca = append(_ca, ctx, folderId, params) _ca = append(_ca, _va...) ret := _m.Called(_ca...) @@ -2127,19 +2128,19 @@ func (_m *MockClientWithResponsesInterface) GetFolderDocumentsWithResponse(ctx c var r0 *queryapi.GetFolderDocumentsResponse var r1 error - if rf, ok := ret.Get(0).(func(context.Context, types.UUID, ...queryapi.RequestEditorFn) (*queryapi.GetFolderDocumentsResponse, error)); ok { - return rf(ctx, folderId, reqEditors...) + if rf, ok := ret.Get(0).(func(context.Context, types.UUID, *queryapi.GetFolderDocumentsParams, ...queryapi.RequestEditorFn) (*queryapi.GetFolderDocumentsResponse, error)); ok { + return rf(ctx, folderId, params, reqEditors...) } - if rf, ok := ret.Get(0).(func(context.Context, types.UUID, ...queryapi.RequestEditorFn) *queryapi.GetFolderDocumentsResponse); ok { - r0 = rf(ctx, folderId, reqEditors...) + if rf, ok := ret.Get(0).(func(context.Context, types.UUID, *queryapi.GetFolderDocumentsParams, ...queryapi.RequestEditorFn) *queryapi.GetFolderDocumentsResponse); ok { + r0 = rf(ctx, folderId, params, reqEditors...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*queryapi.GetFolderDocumentsResponse) } } - if rf, ok := ret.Get(1).(func(context.Context, types.UUID, ...queryapi.RequestEditorFn) error); ok { - r1 = rf(ctx, folderId, reqEditors...) + if rf, ok := ret.Get(1).(func(context.Context, types.UUID, *queryapi.GetFolderDocumentsParams, ...queryapi.RequestEditorFn) error); ok { + r1 = rf(ctx, folderId, params, reqEditors...) } else { r1 = ret.Error(1) } @@ -2155,21 +2156,22 @@ type MockClientWithResponsesInterface_GetFolderDocumentsWithResponse_Call struct // GetFolderDocumentsWithResponse is a helper method to define mock.On call // - ctx context.Context // - folderId types.UUID +// - params *queryapi.GetFolderDocumentsParams // - reqEditors ...queryapi.RequestEditorFn -func (_e *MockClientWithResponsesInterface_Expecter) GetFolderDocumentsWithResponse(ctx interface{}, folderId interface{}, reqEditors ...interface{}) *MockClientWithResponsesInterface_GetFolderDocumentsWithResponse_Call { +func (_e *MockClientWithResponsesInterface_Expecter) GetFolderDocumentsWithResponse(ctx interface{}, folderId interface{}, params interface{}, reqEditors ...interface{}) *MockClientWithResponsesInterface_GetFolderDocumentsWithResponse_Call { return &MockClientWithResponsesInterface_GetFolderDocumentsWithResponse_Call{Call: _e.mock.On("GetFolderDocumentsWithResponse", - append([]interface{}{ctx, folderId}, reqEditors...)...)} + append([]interface{}{ctx, folderId, params}, reqEditors...)...)} } -func (_c *MockClientWithResponsesInterface_GetFolderDocumentsWithResponse_Call) Run(run func(ctx context.Context, folderId types.UUID, reqEditors ...queryapi.RequestEditorFn)) *MockClientWithResponsesInterface_GetFolderDocumentsWithResponse_Call { +func (_c *MockClientWithResponsesInterface_GetFolderDocumentsWithResponse_Call) Run(run func(ctx context.Context, folderId types.UUID, params *queryapi.GetFolderDocumentsParams, reqEditors ...queryapi.RequestEditorFn)) *MockClientWithResponsesInterface_GetFolderDocumentsWithResponse_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]queryapi.RequestEditorFn, len(args)-2) - for i, a := range args[2:] { + variadicArgs := make([]queryapi.RequestEditorFn, len(args)-3) + for i, a := range args[3:] { if a != nil { variadicArgs[i] = a.(queryapi.RequestEditorFn) } } - run(args[0].(context.Context), args[1].(types.UUID), variadicArgs...) + run(args[0].(context.Context), args[1].(types.UUID), args[2].(*queryapi.GetFolderDocumentsParams), variadicArgs...) }) return _c } @@ -2179,7 +2181,7 @@ func (_c *MockClientWithResponsesInterface_GetFolderDocumentsWithResponse_Call) return _c } -func (_c *MockClientWithResponsesInterface_GetFolderDocumentsWithResponse_Call) RunAndReturn(run func(context.Context, types.UUID, ...queryapi.RequestEditorFn) (*queryapi.GetFolderDocumentsResponse, error)) *MockClientWithResponsesInterface_GetFolderDocumentsWithResponse_Call { +func (_c *MockClientWithResponsesInterface_GetFolderDocumentsWithResponse_Call) RunAndReturn(run func(context.Context, types.UUID, *queryapi.GetFolderDocumentsParams, ...queryapi.RequestEditorFn) (*queryapi.GetFolderDocumentsResponse, error)) *MockClientWithResponsesInterface_GetFolderDocumentsWithResponse_Call { _c.Call.Return(run) return _c } diff --git a/pkg/queryAPI/api.gen.go b/pkg/queryAPI/api.gen.go index 5f1cb74a..8f9857ce 100644 --- a/pkg/queryAPI/api.gen.go +++ b/pkg/queryAPI/api.gen.go @@ -574,24 +574,69 @@ type DocClient struct { Name ClientName `json:"name"` } -// Document The document properties. -type Document struct { +// DocumentEnriched Enriched document details with additional metadata +type DocumentEnriched struct { // ClientId The client external id ClientId ClientID `json:"client_id"` // Fields The fields and the value for the document Fields map[string]interface{} `json:"fields"` + // Filename Original filename of the document + Filename *string `json:"filename,omitempty"` + + // FolderId Parent folder ID, null if document is not in a folder + FolderId *openapi_types.UUID `json:"folderId,omitempty"` + + // HasTextRecord Whether a text extraction (field extraction) record exists for this document + HasTextRecord bool `json:"hasTextRecord"` + // Hash The document hash Hash Hash `json:"hash"` // Id The document id. Id DocumentID `json:"id"` + + // Labels All labels applied to this document + Labels []LabelRecord `json:"labels"` + + // OriginalPath Original path provided during upload (immutable) + OriginalPath *string `json:"originalPath,omitempty"` + + // TextRecord Field extraction with version information + TextRecord *FieldExtractionResponse `json:"textRecord,omitempty"` } // DocumentID The document id. type DocumentID = openapi_types.UUID +// DocumentInFolder Document within a folder with enriched metadata (lighter format, excludes client_id and fields) +type DocumentInFolder struct { + // Filename Original filename of the document + Filename *string `json:"filename,omitempty"` + + // FolderId Parent folder ID + FolderId *openapi_types.UUID `json:"folderId,omitempty"` + + // HasTextRecord Whether a text extraction record exists for this document + HasTextRecord bool `json:"hasTextRecord"` + + // Hash The document hash + Hash Hash `json:"hash"` + + // Id The document id. + Id DocumentID `json:"id"` + + // Labels All labels applied to this document + Labels []LabelRecord `json:"labels"` + + // OriginalPath Original path provided during upload (immutable) + OriginalPath *string `json:"originalPath,omitempty"` + + // TextRecord Field extraction with version information + TextRecord *FieldExtractionResponse `json:"textRecord,omitempty"` +} + // DocumentSummary The document summary properties. type DocumentSummary struct { // Hash The document hash @@ -1058,6 +1103,12 @@ type ListClientFoldersParams struct { Metrics *bool `form:"metrics,omitempty" json:"metrics,omitempty"` } +// GetDocumentParams defines parameters for GetDocument. +type GetDocumentParams struct { + // TextRecord When true, includes the full text extraction record in the response + TextRecord *bool `form:"textRecord,omitempty" json:"textRecord,omitempty"` +} + // GetCurrentFieldExtractionParams defines parameters for GetCurrentFieldExtraction. type GetCurrentFieldExtractionParams struct { // DocumentId The document ID @@ -1079,6 +1130,12 @@ type GetFieldExtractionByVersionParams struct { Version int64 `form:"version" json:"version"` } +// GetFolderDocumentsParams defines parameters for GetFolderDocuments. +type GetFolderDocumentsParams struct { + // TextRecord When true, includes the full text extraction record for each document + TextRecord *bool `form:"textRecord,omitempty" json:"textRecord,omitempty"` +} + // GetDocumentsByLabelParams defines parameters for GetDocumentsByLabel. type GetDocumentsByLabelParams struct { // ClientId The client ID to filter documents @@ -1294,7 +1351,7 @@ type ClientInterface interface { ListClients(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) // GetDocument request - GetDocument(ctx context.Context, id DocumentID, reqEditors ...RequestEditorFn) (*http.Response, error) + GetDocument(ctx context.Context, id DocumentID, params *GetDocumentParams, reqEditors ...RequestEditorFn) (*http.Response, error) // GetDocumentLabels request GetDocumentLabels(ctx context.Context, documentId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1332,7 +1389,7 @@ type ClientInterface interface { RenameFolder(ctx context.Context, folderId openapi_types.UUID, body RenameFolderJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) // GetFolderDocuments request - GetFolderDocuments(ctx context.Context, folderId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + GetFolderDocuments(ctx context.Context, folderId openapi_types.UUID, params *GetFolderDocumentsParams, reqEditors ...RequestEditorFn) (*http.Response, error) // GetFolderMetrics request GetFolderMetrics(ctx context.Context, folderId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1722,8 +1779,8 @@ func (c *Client) ListClients(ctx context.Context, reqEditors ...RequestEditorFn) return c.Client.Do(req) } -func (c *Client) GetDocument(ctx context.Context, id DocumentID, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetDocumentRequest(c.Server, id) +func (c *Client) GetDocument(ctx context.Context, id DocumentID, params *GetDocumentParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetDocumentRequest(c.Server, id, params) if err != nil { return nil, err } @@ -1890,8 +1947,8 @@ func (c *Client) RenameFolder(ctx context.Context, folderId openapi_types.UUID, return c.Client.Do(req) } -func (c *Client) GetFolderDocuments(ctx context.Context, folderId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetFolderDocumentsRequest(c.Server, folderId) +func (c *Client) GetFolderDocuments(ctx context.Context, folderId openapi_types.UUID, params *GetFolderDocumentsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetFolderDocumentsRequest(c.Server, folderId, params) if err != nil { return nil, err } @@ -3101,7 +3158,7 @@ func NewListClientsRequest(server string) (*http.Request, error) { } // NewGetDocumentRequest generates requests for GetDocument -func NewGetDocumentRequest(server string, id DocumentID) (*http.Request, error) { +func NewGetDocumentRequest(server string, id DocumentID, params *GetDocumentParams) (*http.Request, error) { var err error var pathParam0 string @@ -3126,6 +3183,28 @@ func NewGetDocumentRequest(server string, id DocumentID) (*http.Request, error) return nil, err } + if params != nil { + queryValues := queryURL.Query() + + if params.TextRecord != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "textRecord", runtime.ParamLocationQuery, *params.TextRecord); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err @@ -3524,7 +3603,7 @@ func NewRenameFolderRequestWithBody(server string, folderId openapi_types.UUID, } // NewGetFolderDocumentsRequest generates requests for GetFolderDocuments -func NewGetFolderDocumentsRequest(server string, folderId openapi_types.UUID) (*http.Request, error) { +func NewGetFolderDocumentsRequest(server string, folderId openapi_types.UUID, params *GetFolderDocumentsParams) (*http.Request, error) { var err error var pathParam0 string @@ -3549,6 +3628,28 @@ func NewGetFolderDocumentsRequest(server string, folderId openapi_types.UUID) (* return nil, err } + if params != nil { + queryValues := queryURL.Query() + + if params.TextRecord != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "textRecord", runtime.ParamLocationQuery, *params.TextRecord); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err @@ -4101,7 +4202,7 @@ type ClientWithResponsesInterface interface { ListClientsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListClientsResponse, error) // GetDocumentWithResponse request - GetDocumentWithResponse(ctx context.Context, id DocumentID, reqEditors ...RequestEditorFn) (*GetDocumentResponse, error) + GetDocumentWithResponse(ctx context.Context, id DocumentID, params *GetDocumentParams, reqEditors ...RequestEditorFn) (*GetDocumentResponse, error) // GetDocumentLabelsWithResponse request GetDocumentLabelsWithResponse(ctx context.Context, documentId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetDocumentLabelsResponse, error) @@ -4139,7 +4240,7 @@ type ClientWithResponsesInterface interface { RenameFolderWithResponse(ctx context.Context, folderId openapi_types.UUID, body RenameFolderJSONRequestBody, reqEditors ...RequestEditorFn) (*RenameFolderResponse, error) // GetFolderDocumentsWithResponse request - GetFolderDocumentsWithResponse(ctx context.Context, folderId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetFolderDocumentsResponse, error) + GetFolderDocumentsWithResponse(ctx context.Context, folderId openapi_types.UUID, params *GetFolderDocumentsParams, reqEditors ...RequestEditorFn) (*GetFolderDocumentsResponse, error) // GetFolderMetricsWithResponse request GetFolderMetricsWithResponse(ctx context.Context, folderId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetFolderMetricsResponse, error) @@ -4797,9 +4898,10 @@ func (r ListClientsResponse) StatusCode() int { type GetDocumentResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *Document + JSON200 *DocumentEnriched JSON400 *InvalidRequest JSON401 *Unauthorized + JSON404 *NotFound JSON429 *TooManyRequests JSON500 *InternalError } @@ -5069,7 +5171,7 @@ type GetFolderDocumentsResponse struct { Body []byte HTTPResponse *http.Response JSON200 *struct { - Documents *[]Document `json:"documents,omitempty"` + Documents *[]DocumentInFolder `json:"documents,omitempty"` } JSON400 *InvalidRequest JSON401 *Unauthorized @@ -5634,8 +5736,8 @@ func (c *ClientWithResponses) ListClientsWithResponse(ctx context.Context, reqEd } // GetDocumentWithResponse request returning *GetDocumentResponse -func (c *ClientWithResponses) GetDocumentWithResponse(ctx context.Context, id DocumentID, reqEditors ...RequestEditorFn) (*GetDocumentResponse, error) { - rsp, err := c.GetDocument(ctx, id, reqEditors...) +func (c *ClientWithResponses) GetDocumentWithResponse(ctx context.Context, id DocumentID, params *GetDocumentParams, reqEditors ...RequestEditorFn) (*GetDocumentResponse, error) { + rsp, err := c.GetDocument(ctx, id, params, reqEditors...) if err != nil { return nil, err } @@ -5756,8 +5858,8 @@ func (c *ClientWithResponses) RenameFolderWithResponse(ctx context.Context, fold } // GetFolderDocumentsWithResponse request returning *GetFolderDocumentsResponse -func (c *ClientWithResponses) GetFolderDocumentsWithResponse(ctx context.Context, folderId openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetFolderDocumentsResponse, error) { - rsp, err := c.GetFolderDocuments(ctx, folderId, reqEditors...) +func (c *ClientWithResponses) GetFolderDocumentsWithResponse(ctx context.Context, folderId openapi_types.UUID, params *GetFolderDocumentsParams, reqEditors ...RequestEditorFn) (*GetFolderDocumentsResponse, error) { + rsp, err := c.GetFolderDocuments(ctx, folderId, params, reqEditors...) if err != nil { return nil, err } @@ -7249,7 +7351,7 @@ func ParseGetDocumentResponse(rsp *http.Response) (*GetDocumentResponse, error) switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest Document + var dest DocumentEnriched if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } @@ -7269,6 +7371,13 @@ func ParseGetDocumentResponse(rsp *http.Response) (*GetDocumentResponse, error) } response.JSON401 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFound + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 429: var dest TooManyRequests if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -7843,7 +7952,7 @@ func ParseGetFolderDocumentsResponse(rsp *http.Response) (*GetFolderDocumentsRes switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: var dest struct { - Documents *[]Document `json:"documents,omitempty"` + Documents *[]DocumentInFolder `json:"documents,omitempty"` } if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err diff --git a/serviceAPIs/queryAPI.yaml b/serviceAPIs/queryAPI.yaml index 2c93a3cb..6f66be07 100644 --- a/serviceAPIs/queryAPI.yaml +++ b/serviceAPIs/queryAPI.yaml @@ -693,9 +693,19 @@ paths: tags: - DocumentsService summary: Get document details by its id - description: Retrieves the details of a document by its id. + description: | + Retrieves the details of a document by its id, including metadata such as + folder, filename, labels, and optionally the full text extraction record. security: - jwtAuth: [] + parameters: + - name: textRecord + in: query + required: false + description: When true, includes the full text extraction record in the response + schema: + type: boolean + default: false responses: "200": description: Document details. @@ -705,11 +715,13 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Document" + $ref: "#/components/schemas/DocumentEnriched" "400": $ref: "#/components/responses/InvalidRequest" "401": $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": @@ -811,9 +823,20 @@ paths: tags: - FolderService summary: Get documents in a folder - description: Retrieves all documents within a specific folder. + description: | + Retrieves all documents within a specific folder, including filename and labels. + Optionally includes the full text extraction record for each document. + Returns a lighter-weight format than GET /document/{id} (excludes client_id and computed fields). security: - jwtAuth: [] + parameters: + - name: textRecord + in: query + required: false + description: When true, includes the full text extraction record for each document + schema: + type: boolean + default: false responses: "200": description: List of documents in folder. @@ -829,7 +852,7 @@ paths: type: array maxItems: 10000 items: - $ref: "#/components/schemas/Document" + $ref: "#/components/schemas/DocumentInFolder" "400": $ref: "#/components/responses/InvalidRequest" "401": @@ -2383,9 +2406,16 @@ components: - id - hash - Document: - description: The document properties. + DocumentEnriched: + description: Enriched document details with additional metadata type: object + required: + - id + - client_id + - hash + - fields + - hasTextRecord + - labels properties: id: $ref: "#/components/schemas/DocumentID" @@ -2400,12 +2430,68 @@ components: property1: "string value" property2: 42 property3: true - property5: [1, 2, 3] + hasTextRecord: + type: boolean + description: "Whether a text extraction (field extraction) record exists for this document" + folderId: + type: string + format: uuid + maxLength: 50 + description: "Parent folder ID, null if document is not in a folder" + filename: + type: string + maxLength: 4096 + description: "Original filename of the document" + originalPath: + type: string + maxLength: 4096 + description: "Original path provided during upload (immutable)" + labels: + type: array + maxItems: 1000 + items: + $ref: "#/components/schemas/LabelRecord" + description: "All labels applied to this document" + textRecord: + $ref: "#/components/schemas/FieldExtractionResponse" + + DocumentInFolder: + description: Document within a folder with enriched metadata (lighter format, excludes client_id and fields) + type: object required: - id - - client_id - hash - - fields + - hasTextRecord + - labels + properties: + id: + $ref: "#/components/schemas/DocumentID" + hash: + $ref: "#/components/schemas/Hash" + hasTextRecord: + type: boolean + description: "Whether a text extraction record exists for this document" + folderId: + type: string + format: uuid + maxLength: 50 + description: "Parent folder ID" + filename: + type: string + maxLength: 4096 + description: "Original filename of the document" + originalPath: + type: string + maxLength: 4096 + description: "Original path provided during upload (immutable)" + labels: + type: array + maxItems: 1000 + items: + $ref: "#/components/schemas/LabelRecord" + description: "All labels applied to this document" + textRecord: + $ref: "#/components/schemas/FieldExtractionResponse" ClientCreate: description: The properties for creation. diff --git a/test/process_test.go b/test/process_test.go index 8e9d5c3c..d979ccc5 100644 --- a/test/process_test.go +++ b/test/process_test.go @@ -78,20 +78,33 @@ func TestProcess(t *testing.T) { assert.Len(t, *docs.JSON200, 1) doc := (*docs.JSON200)[0] - expectedDoc := queryapi.Document{ - Id: doc.Id, - Hash: doc.Hash, - ClientId: clientId, - Fields: map[string]any{ - "JSON_QUERY": "valueone", - }, - } - - docRes, err := net.Client.GetDocumentWithResponse(t.Context(), doc.Id) + docRes, err := net.Client.GetDocumentWithResponse(t.Context(), doc.Id, nil) require.NoError(t, err) require.NotNil(t, docRes) require.NotNil(t, docRes.JSON200) - assert.EqualExportedValues(t, expectedDoc, *docRes.JSON200) + + // Verify core document fields + assert.Equal(t, doc.Id, docRes.JSON200.Id) + assert.Equal(t, doc.Hash, docRes.JSON200.Hash) + assert.Equal(t, clientId, docRes.JSON200.ClientId) + assert.Equal(t, map[string]any{"JSON_QUERY": "valueone"}, docRes.JSON200.Fields) + assert.Equal(t, []queryapi.LabelRecord{}, docRes.JSON200.Labels) + + // Verify enriched fields are populated (values depend on upload) + assert.NotNil(t, docRes.JSON200.Filename, "Filename should be populated") + assert.Equal(t, "helloworld.pdf", *docRes.JSON200.Filename) + assert.NotNil(t, docRes.JSON200.OriginalPath, "OriginalPath should be populated") + + // Test with textRecord=true parameter + includeTextRecord := true + docResWithText, err := net.Client.GetDocumentWithResponse(t.Context(), doc.Id, &queryapi.GetDocumentParams{ + TextRecord: &includeTextRecord, + }) + require.NoError(t, err) + require.NotNil(t, docResWithText.JSON200) + // Document doesn't have a field extraction in this test, so hasTextRecord should be false + assert.False(t, docResWithText.JSON200.HasTextRecord) + assert.Nil(t, docResWithText.JSON200.TextRecord) }) t.Run("update config", func(t *testing.T) { cfg := &Config{} @@ -158,20 +171,22 @@ func TestProcess(t *testing.T) { assert.Len(t, *docs.JSON200, 1) doc := (*docs.JSON200)[0] - expectedDoc := queryapi.Document{ - Id: doc.Id, - Hash: doc.Hash, - ClientId: clientId, - Fields: map[string]any{ - "JSON_QUERY": "valuetwo", - }, - } - - docRes, err := net.Client.GetDocumentWithResponse(t.Context(), doc.Id) + docRes, err := net.Client.GetDocumentWithResponse(t.Context(), doc.Id, nil) require.NoError(t, err) require.NotNil(t, docRes) require.NotNil(t, docRes.JSON200) - assert.EqualExportedValues(t, expectedDoc, *docRes.JSON200) + + // Verify core document fields + assert.Equal(t, doc.Id, docRes.JSON200.Id) + assert.Equal(t, doc.Hash, docRes.JSON200.Hash) + assert.Equal(t, clientId, docRes.JSON200.ClientId) + assert.Equal(t, map[string]any{"JSON_QUERY": "valuetwo"}, docRes.JSON200.Fields) + assert.Equal(t, []queryapi.LabelRecord{}, docRes.JSON200.Labels) + + // Verify enriched fields are populated (values depend on upload) + assert.NotNil(t, docRes.JSON200.Filename, "Filename should be populated") + assert.Equal(t, "helloworld.pdf", *docRes.JSON200.Filename) + assert.NotNil(t, docRes.JSON200.OriginalPath, "OriginalPath should be populated") }) t.Run("test multiple versions", func(t *testing.T) { cfg := &Config{} diff --git a/test/text_extraction_integration_test.go b/test/text_extraction_integration_test.go index 70067cd6..bbcf30e7 100644 --- a/test/text_extraction_integration_test.go +++ b/test/text_extraction_integration_test.go @@ -764,7 +764,7 @@ func TestFolderAPI(t *testing.T) { // Test folder documents endpoint (should be empty initially) fmt.Println("\n=== Testing folder documents (empty folders) ===") - folderDocsRes, err := net.Client.GetFolderDocumentsWithResponse(t.Context(), subfolder2ID) + folderDocsRes, err := net.Client.GetFolderDocumentsWithResponse(t.Context(), subfolder2ID, nil) require.NoError(t, err, "Failed to get subfolder2 documents") require.NotNil(t, folderDocsRes.JSON200, "Expected 200 for folder documents") docCount := 0 @@ -1005,7 +1005,7 @@ func TestDeepFolderHierarchy(t *testing.T) { // Verify we can get documents for the deepest folder (should be empty) fmt.Println("\n=== Testing documents for deepest folder ===") - docsRes, err := net.Client.GetFolderDocumentsWithResponse(t.Context(), deepestFolderID) + docsRes, err := net.Client.GetFolderDocumentsWithResponse(t.Context(), deepestFolderID, nil) require.NoError(t, err, "Failed to get documents for deepest folder") require.NotNil(t, docsRes.JSON200, "Expected 200 for folder documents") docCount := 0