Merged in feature/postprocessing (pull request #114)

Feature/postprocessing

* tests

* passtest

* fixshorttests

* mosttests

* improvingbasedockerfile

* testspeeds

* testing

* host

* canparallel

* clean

* passfullsuite

* singlepagemax

* test

* findfeatures

* findstables

* tbls

* tablestoo

* tablestoo

* lateraltests

* tableloc

* cleanup

* inlinetable

* childids

* cleanup

* tests
This commit is contained in:
Michael McGuinness
2025-04-22 14:40:16 +00:00
parent edc50c7510
commit fee71e7740
404 changed files with 211048 additions and 2820 deletions
+3 -7
View File
@@ -5,8 +5,6 @@ import (
"log/slog"
clientsync "queryorchestration/internal/client/sync"
"github.com/go-playground/validator/v10"
)
const Name = "clientSyncRunner"
@@ -16,14 +14,12 @@ type Services struct {
}
type Runner struct {
validator *validator.Validate
svc *Services
svc *Services
}
func New(validator *validator.Validate, svc *Services) Runner {
func New(svc *Services) Runner {
return Runner{
validator: validator,
svc: svc,
svc: svc,
}
}
+1 -2
View File
@@ -13,7 +13,6 @@ import (
queuemock "queryorchestration/mocks/queue"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/pashagolub/pgxmock/v3"
"github.com/stretchr/testify/assert"
@@ -41,7 +40,7 @@ func TestQueryRunner(t *testing.T) {
svc := clientsync.New(cfg)
runner := clientsyncrunner.New(validator.New(), &clientsyncrunner.Services{
runner := clientsyncrunner.New(&clientsyncrunner.Services{
ClientSync: svc,
})
assert.NotNil(t, runner)
+3 -6
View File
@@ -6,7 +6,6 @@ import (
documentclean "queryorchestration/internal/document/clean"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
@@ -17,14 +16,12 @@ type Services struct {
}
type Runner struct {
validator *validator.Validate
svc *Services
svc *Services
}
func New(validator *validator.Validate, svc *Services) Runner {
func New(svc *Services) Runner {
return Runner{
validator: validator,
svc: svc,
svc: svc,
}
}
+1 -2
View File
@@ -20,7 +20,6 @@ import (
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/pashagolub/pgxmock/v3"
"github.com/stretchr/testify/assert"
@@ -49,7 +48,7 @@ func TestDocCleanRunner(t *testing.T) {
cfg.QueueClient = mockSQS
cfg.DocumentTextTriggerURL = "/i/am/here"
runner := doccleanrunner.New(validator.New(), &doccleanrunner.Services{
runner := doccleanrunner.New(&doccleanrunner.Services{
Clean: documentclean.New(cfg),
})
assert.NotNil(t, runner)
+3 -7
View File
@@ -6,8 +6,6 @@ import (
documentinit "queryorchestration/internal/document/init"
"queryorchestration/internal/serviceconfig/objectstore"
"github.com/go-playground/validator/v10"
)
const Name = "docInitRunner"
@@ -17,14 +15,12 @@ type Services struct {
}
type Runner struct {
validator *validator.Validate
svc *Services
svc *Services
}
func New(validator *validator.Validate, svc *Services) Runner {
func New(svc *Services) Runner {
return Runner{
validator: validator,
svc: svc,
svc: svc,
}
}
+1 -2
View File
@@ -16,7 +16,6 @@ import (
queuemock "queryorchestration/mocks/queue"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/pashagolub/pgxmock/v3"
"github.com/stretchr/testify/assert"
@@ -41,7 +40,7 @@ func TestDocInitRunner(t *testing.T) {
mockSQS := queuemock.NewMockSQSClient(t)
cfg.QueueClient = mockSQS
runner := docinitrunner.New(validator.New(), &docinitrunner.Services{
runner := docinitrunner.New(&docinitrunner.Services{
Document: documentinit.New(cfg),
})
assert.NotNil(t, runner)
+3 -6
View File
@@ -6,7 +6,6 @@ import (
documentsync "queryorchestration/internal/document/sync"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
@@ -17,14 +16,12 @@ type Services struct {
}
type Runner struct {
validator *validator.Validate
svc *Services
svc *Services
}
func New(validator *validator.Validate, svc *Services) Runner {
func New(svc *Services) Runner {
return Runner{
validator: validator,
svc: svc,
svc: svc,
}
}
+1 -2
View File
@@ -15,7 +15,6 @@ import (
queuemock "queryorchestration/mocks/queue"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/pashagolub/pgxmock/v3"
"github.com/stretchr/testify/assert"
@@ -40,7 +39,7 @@ func TestDocInitRunner(t *testing.T) {
mockSQS := queuemock.NewMockSQSClient(t)
cfg.QueueClient = mockSQS
runner := docsyncrunner.New(validator.New(), &docsyncrunner.Services{
runner := docsyncrunner.New(&docsyncrunner.Services{
Document: documentsync.New(cfg, &documentsync.Services{
Document: document.New(cfg),
Client: client.New(cfg),
-55
View File
@@ -1,55 +0,0 @@
package doctextprocessrunner
import (
"context"
"log/slog"
documenttext "queryorchestration/internal/document/text"
"queryorchestration/internal/serviceconfig/objectstore"
"github.com/go-playground/validator/v10"
)
const Name = "docTextProcessRunner"
type Services struct {
Text *documenttext.Service
}
type Runner struct {
validator *validator.Validate
svc *Services
}
func New(validator *validator.Validate, svc *Services) Runner {
return Runner{
validator: validator,
svc: svc,
}
}
type Body struct {
Bucket string `json:"bucket" validate:"required"`
Key string `json:"key" validate:"required"`
Hash string `json:"hash" validate:"required"`
}
func (s Runner) Process(ctx context.Context, body Body) bool {
key, err := objectstore.ParseBucketKey(body.Key)
if err != nil {
slog.Error("unable to parse key", "key", body.Key)
return true
}
err = s.svc.Text.Process(ctx, &documenttext.ProcessParams{
Bucket: body.Bucket,
Key: key,
Hash: body.Hash,
})
if err != nil {
slog.Error("unable to process", "bucket", body.Bucket, "key", body.Key, "hash", body.Hash, "error", err)
return false
}
return true
}
-155
View File
@@ -1,155 +0,0 @@
package doctextprocessrunner
import (
"context"
"errors"
"fmt"
"io"
"strings"
"testing"
"time"
"queryorchestration/internal/database/repository"
"queryorchestration/internal/document"
documenttext "queryorchestration/internal/document/text"
"queryorchestration/internal/server/runner"
"queryorchestration/internal/serviceconfig/aws"
"queryorchestration/internal/serviceconfig/objectstore"
"queryorchestration/internal/serviceconfig/queue/querysync"
"queryorchestration/internal/serviceconfig/textract"
objectstoremock "queryorchestration/mocks/objectstore"
queuemock "queryorchestration/mocks/queue"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/pashagolub/pgxmock/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type DocInitConfig struct {
runner.BaseConfig[Body]
aws.AWSConfig
textract.TextractConfig
querysync.QuerySyncConfig
objectstore.ObjectStoreConfig
}
func TestDocTextProcessRunner(t *testing.T) {
ctx := context.Background()
pool, err := pgxmock.NewPool()
require.NoError(t, err)
cfg := &DocInitConfig{}
cfg.DBPool = pool
cfg.DBQueries = repository.New(pool)
mockSQS := queuemock.NewMockSQSClient(t)
cfg.QueueClient = mockSQS
mockS3 := objectstoremock.NewMockS3Client(t)
cfg.StoreClient = mockS3
runner := New(validator.New(), &Services{
Text: documenttext.New(cfg),
})
assert.NotNil(t, runner)
t.Run("pass through", func(t *testing.T) {
triggerId := uuid.New()
docId := uuid.New()
textractId := "heeloo"
textId := uuid.New()
cleanId := uuid.New()
bucketName := "bucketName"
docinfo := document.DocumentSummary{
ID: uuid.New(),
ClientID: "hello",
Hash: `"5d41402abc4b2a76b9719d911017c592"`,
}
key := objectstore.BucketKey{
ClientID: docinfo.ClientID,
Location: objectstore.TextTextract,
EntityID: triggerId,
CreatedAt: time.Now().UTC(),
}
doc := Body{
Bucket: bucketName,
Key: key.String(),
Hash: docinfo.Hash,
}
pool.ExpectQuery("name: GetDocumentTextTrigger :one").WithArgs(triggerId).WillReturnRows(
pgxmock.NewRows([]string{"id", "cleanId", "version", "textractJobId", "documentId"}).
AddRow(triggerId, cleanId, int64(123), &textractId, docId),
)
pool.ExpectBegin()
pool.ExpectQuery("name: GetDocumentTextExtractionByHash :one").WithArgs(cleanId, docinfo.Hash).WillReturnRows(
pgxmock.NewRows([]string{"id"}).
AddRow(textId),
)
pool.ExpectExec("name: AddDocumentTextEntry :exec").WithArgs(textId, triggerId, pgxmock.AnyArg()).
WillReturnResult(pgxmock.NewResult("", 1))
pool.ExpectCommit()
body := io.NopCloser(strings.NewReader("hello"))
mockS3.EXPECT().
GetObject(
mock.Anything,
mock.MatchedBy(func(in *s3.GetObjectInput) bool {
return *in.IfMatch == `"5d41402abc4b2a76b9719d911017c592"`
}),
mock.Anything,
).
Return(&s3.GetObjectOutput{
Body: body,
}, nil)
mockSQS.EXPECT().
SendMessage(
mock.Anything,
mock.MatchedBy(func(in *sqs.SendMessageInput) bool {
return *in.QueueUrl == cfg.QuerySyncURL && *in.MessageBody == fmt.Sprintf("{\"id\":\"%s\"}", docId.String())
}),
mock.Anything,
).
Return(&sqs.SendMessageOutput{}, nil)
assert.True(t, runner.Process(ctx, doc))
})
t.Run("invalid key", func(t *testing.T) {
key := objectstore.BucketKey{}
doc := Body{
Key: key.String(),
}
assert.True(t, runner.Process(ctx, doc))
})
t.Run("unable to process", func(t *testing.T) {
triggerId := uuid.New()
bucketName := "bucketName"
docinfo := document.DocumentSummary{
ID: uuid.New(),
ClientID: "hello",
Hash: `"5d41402abc4b2a76b9719d911017c592"`,
}
key := objectstore.BucketKey{
ClientID: docinfo.ClientID,
Location: objectstore.TextTextract,
EntityID: triggerId,
CreatedAt: time.Now().UTC(),
}
doc := Body{
Bucket: bucketName,
Key: key.String(),
Hash: docinfo.Hash,
}
pool.ExpectQuery("name: GetDocumentTextTrigger :one").WithArgs(triggerId).
WillReturnError(errors.New("db fail"))
assert.False(t, runner.Process(ctx, doc))
})
}
+3 -6
View File
@@ -6,7 +6,6 @@ import (
documenttext "queryorchestration/internal/document/text"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
@@ -17,14 +16,12 @@ type Services struct {
}
type Runner struct {
validator *validator.Validate
svc *Services
svc *Services
}
func New(validator *validator.Validate, svc *Services) Runner {
func New(svc *Services) Runner {
return Runner{
validator: validator,
svc: svc,
svc: svc,
}
}
+153 -62
View File
@@ -2,6 +2,11 @@ package doctextrunner_test
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"strings"
"testing"
"time"
@@ -17,8 +22,9 @@ import (
queuemock "queryorchestration/mocks/queue"
textractmock "queryorchestration/mocks/textract"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/sqs"
awstextract "github.com/aws/aws-sdk-go-v2/service/textract"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/pashagolub/pgxmock/v3"
"github.com/stretchr/testify/assert"
@@ -36,84 +42,169 @@ type DocTextConfig struct {
func TestDocCleanRunner(t *testing.T) {
ctx := context.Background()
pool, err := pgxmock.NewPool()
require.NoError(t, err)
cfg := &DocTextConfig{}
cfg.DBPool = pool
cfg.DBQueries = repository.New(pool)
mockSQS := queuemock.NewMockSQSClient(t)
cfg.QueueClient = mockSQS
cfg.QuerySyncURL = "/i/am/here"
mockS3 := objectstoremock.NewMockS3Client(t)
cfg.StoreClient = mockS3
mockStore := objectstoremock.NewMockS3Client(t)
cfg.StoreClient = mockStore
mockTextract := textractmock.NewMockTextractClient(t)
cfg.TextractClient = mockTextract
mockQueue := queuemock.NewMockSQSClient(t)
cfg.QueueClient = mockQueue
cfg.QuerySyncURL = "ex"
runner := doctextrunner.New(validator.New(), &doctextrunner.Services{
runner := doctextrunner.New(&doctextrunner.Services{
Text: documenttext.New(cfg),
})
assert.NotNil(t, runner)
doc := doctextrunner.Body{
DocumentID: uuid.New(),
}
t.Run("valid", func(t *testing.T) {
doc := doctextrunner.Body{
DocumentID: uuid.New(),
}
cleanId := uuid.New()
bucket := "bucket"
hash := `"a06d5e0e795adeb6d0b3732330dbcc15"`
textId := uuid.New()
key := objectstore.BucketKey{
ClientID: "aa",
Location: objectstore.Import,
CreatedAt: time.Now().UTC(),
EntityID: cleanId,
}
keyStr := key.String()
filename := "../../assets/sampleGeneration/generated/helloWorld.gen"
file, err := os.Open(filename)
require.NoError(t, err)
defer file.Close()
cleanId := uuid.New()
bucket := "hello"
key := objectstore.BucketKey{
CreatedAt: time.Now().UTC(),
ClientID: "hi",
EntityID: uuid.New(),
Location: objectstore.Import,
}
keyStr := key.String()
hash := "example"
decoder := json.NewDecoder(file)
var textractBaseOut map[string][]awstextract.AnalyzeDocumentOutput
err = decoder.Decode(&textractBaseOut)
require.NoError(t, err)
pool.ExpectQuery("name: IsDocumentTextExtracted :one").WithArgs(doc.DocumentID).WillReturnRows(
pgxmock.NewRows([]string{"isextracted"}).
AddRow(false),
mimetype := repository.NullCleanmimetype{
Valid: true,
Cleanmimetype: repository.CleanmimetypeApplicationPdf,
}
pool.ExpectQuery("name: IsDocumentTextExtracted :one").WithArgs(doc.DocumentID).WillReturnRows(
pgxmock.NewRows([]string{"isextracted"}).
AddRow(false),
)
pool.ExpectQuery("name: GetCleanEntryByDocId :one").WithArgs(doc.DocumentID).
WillReturnRows(
pgxmock.NewRows([]string{"id", "documentId", "bucket", "key", "version", "hash", "mimetype", "fail", "clientId"}).
AddRow(cleanId, doc.DocumentID, &bucket, &keyStr, int64(123), &hash, mimetype, nil, key.ClientID),
)
mimeType := "application/pdf"
dbmimetype := repository.NullCleanmimetype{
Valid: true,
Cleanmimetype: repository.Cleanmimetype(mimeType),
}
pool.ExpectQuery("name: GetCleanEntryByDocId :one").WithArgs(doc.DocumentID).WillReturnRows(
pgxmock.NewRows([]string{"id", "documentId", "bucket", "key", "version", "hash", "mimetype", "fail", "externalClientId"}).
AddRow(cleanId, doc.DocumentID, &bucket, &keyStr, int64(1), &hash, dbmimetype, repository.NullCleanfailtype{}, key.ClientID),
)
jobId := "hello"
triggerId := uuid.New()
pool.ExpectBegin()
pool.ExpectQuery("name: GetTextractOutputCurrentPart :one").WithArgs(&key.ClientID, pgxmock.AnyArg()).
WillReturnRows(
pgxmock.NewRows([]string{"part", "count"}).
AddRow(0, 2),
)
pool.ExpectQuery("name: AddDocumentTextTrigger :one").WithArgs(cleanId, pgxmock.AnyArg(), pgxmock.AnyArg(), uint16(0)).WillReturnRows(
pgxmock.NewRows([]string{"id"}).AddRow(triggerId),
)
mockTextract.EXPECT().
StartDocumentTextDetection(
mock.Anything,
mock.MatchedBy(func(in *awstextract.StartDocumentTextDetectionInput) bool {
bodyMsg := io.NopCloser(strings.NewReader(pdfHelloWorld))
mockStore.EXPECT().
GetObject(
mock.Anything,
mock.MatchedBy(func(in *s3.GetObjectInput) bool {
return *in.Bucket == bucket && *in.Key == keyStr
}),
mock.Anything,
).
Return(&s3.GetObjectOutput{
Body: bodyMsg,
}, nil)
sent := 0
mockTextract.EXPECT().
AnalyzeDocument(
mock.Anything,
mock.MatchedBy(func(in *awstextract.AnalyzeDocumentInput) bool {
if sent == 0 {
sent++
return true
}),
mock.Anything,
).
Return(&awstextract.StartDocumentTextDetectionOutput{
JobId: &jobId,
}, nil)
}
return false
}),
mock.Anything,
).
Return(&textractBaseOut["base"][0], nil).Once()
pool.ExpectExec("name: AddDocumentTextTriggerJobId :exec").WithArgs(&jobId, triggerId).
WillReturnResult(pgxmock.NewResult("", 1))
pool.ExpectCommit()
pool.ExpectBegin()
pool.ExpectQuery("name: GetDocumentTextExtractionByHash :one").WithArgs(cleanId, hash).WillReturnRows(
pgxmock.NewRows([]string{"id"}).
AddRow(textId),
)
pool.ExpectExec("name: AddDocumentTextEntry :exec").WithArgs(textId, pgxmock.AnyArg()).
WillReturnResult(pgxmock.NewResult("", 1))
pool.ExpectCommit()
assert.True(t, runner.Process(ctx, doc))
})
mockQueue.EXPECT().
SendMessage(
mock.Anything,
mock.MatchedBy(func(in *sqs.SendMessageInput) bool {
return *in.QueueUrl == cfg.QuerySyncURL && *in.MessageBody == fmt.Sprintf("{\"id\":\"%s\"}", doc.DocumentID.String())
}),
mock.Anything,
).
Return(&sqs.SendMessageOutput{}, nil)
assert.True(t, runner.Process(ctx, doc))
}
const pdfHelloWorld = `%PDF-1.4
%
1 0 obj
<<
/Type /Catalog
/Pages 2 0 R
/Version /1.4
>>
endobj
2 0 obj
<<
/Type /Pages
/Kids [3 0 R]
/Count 1
>>
endobj
3 0 obj
<<
/Type /Page
/Parent 2 0 R
/MediaBox [0 0 612 792]
/Resources <<
/Font <<
/F1 <<
/Type /Font
/Subtype /Type1
/BaseFont /Helvetica
>>
>>
>>
/Contents 4 0 R
>>
endobj
4 0 obj
<<
/Length
44
>>
stream
BT
/F1 24 Tf
100 700 Td
(Hello World!) Tj
ET
endstream
endobj
xref
0 5
0000000000 65535 f
0000000015 00000 n
0000000086 00000 n
0000000151 00000 n
0000000376 00000 n
trailer
<<
/Size 5
/Root 1 0 R
>>
startxref
472
%%EOF`
+3 -6
View File
@@ -6,7 +6,6 @@ import (
resultset "queryorchestration/internal/query/result/set"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
@@ -17,14 +16,12 @@ type Services struct {
}
type Runner struct {
validator *validator.Validate
svc *Services
svc *Services
}
func New(validator *validator.Validate, svc *Services) Runner {
func New(svc *Services) Runner {
return Runner{
validator: validator,
svc: svc,
svc: svc,
}
}
+3 -4
View File
@@ -17,7 +17,6 @@ import (
queuemock "queryorchestration/mocks/queue"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/pashagolub/pgxmock/v3"
"github.com/stretchr/testify/assert"
@@ -44,7 +43,7 @@ func TestQueryRunner(t *testing.T) {
cfg.QueryURL = "/i/am/here"
que := query.New(cfg)
runner := queryrunner.New(validator.New(), &queryrunner.Services{
runner := queryrunner.New(&queryrunner.Services{
ResultSet: resultset.New(cfg, &resultset.Services{
Query: que,
Result: result.New(cfg, &result.Services{
@@ -76,8 +75,8 @@ func TestQueryRunner(t *testing.T) {
textEntryId := uuid.New()
pool.ExpectQuery("name: GetTextEntryByDocId :one").WithArgs(params.DocumentID).
WillReturnRows(
pgxmock.NewRows([]string{"id", "documentId", "bucket", "key", "hash", "cleanId", "triggerId", "textractJobId", "triggerVersion", "extractionVersion"}).
AddRow(textEntryId, params.DocumentID, "buket", "/i/am/here", "example", uuid.New(), uuid.New(), "jobId", int64(123), int64(543)),
pgxmock.NewRows([]string{"id", "documentId", "bucket", "key", "hash", "cleanId", "extractionVersion"}).
AddRow(textEntryId, params.DocumentID, "buket", "/i/am/here", "example", uuid.New(), int64(543)),
)
pool.ExpectQuery("name: GetQuery :one").WithArgs(query.ID).WillReturnRows(
pgxmock.NewRows([]string{"id", "type", "activeVersion", "latestVersion", "config", "requiredIds"}).
+3 -6
View File
@@ -6,7 +6,6 @@ import (
querysync "queryorchestration/internal/query/sync"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
@@ -17,14 +16,12 @@ type Services struct {
}
type Runner struct {
validator *validator.Validate
svc *Services
svc *Services
}
func New(validator *validator.Validate, svc *Services) Runner {
func New(svc *Services) Runner {
return Runner{
validator: validator,
svc: svc,
svc: svc,
}
}
+1 -2
View File
@@ -14,7 +14,6 @@ import (
queuemock "queryorchestration/mocks/queue"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/pashagolub/pgxmock/v3"
"github.com/stretchr/testify/assert"
@@ -44,7 +43,7 @@ func TestQueryRunner(t *testing.T) {
ResultSync: resultsync.New(cfg),
})
runner := querysyncrunner.New(validator.New(), &querysyncrunner.Services{
runner := querysyncrunner.New(&querysyncrunner.Services{
QuerySync: svc,
})
assert.NotNil(t, runner)
+3 -6
View File
@@ -6,7 +6,6 @@ import (
queryversionsync "queryorchestration/internal/query/versionsync"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
@@ -17,14 +16,12 @@ type Services struct {
}
type Runner struct {
validator *validator.Validate
svc *Services
svc *Services
}
func New(validator *validator.Validate, svc *Services) Runner {
func New(svc *Services) Runner {
return Runner{
validator: validator,
svc: svc,
svc: svc,
}
}
+1 -2
View File
@@ -13,7 +13,6 @@ import (
queuemock "queryorchestration/mocks/queue"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/pashagolub/pgxmock/v3"
"github.com/stretchr/testify/assert"
@@ -41,7 +40,7 @@ func TestQueryRunner(t *testing.T) {
svc := queryversionsync.New(cfg)
runner := queryversionsyncrunner.New(validator.New(), &queryversionsyncrunner.Services{
runner := queryversionsyncrunner.New(&queryversionsyncrunner.Services{
Sync: svc,
})
assert.NotNil(t, runner)
+3 -7
View File
@@ -5,8 +5,6 @@ import (
"log/slog"
documentstore "queryorchestration/internal/document/store"
"github.com/go-playground/validator/v10"
)
const Name = "storeEventRunner"
@@ -16,14 +14,12 @@ type Services struct {
}
type Runner struct {
validator *validator.Validate
svc *Services
svc *Services
}
func New(validator *validator.Validate, svc *Services) Runner {
func New(svc *Services) Runner {
return Runner{
validator: validator,
svc: svc,
svc: svc,
}
}
+1 -5
View File
@@ -11,11 +11,9 @@ import (
"queryorchestration/internal/server/runner"
"queryorchestration/internal/serviceconfig/objectstore"
"queryorchestration/internal/serviceconfig/queue/documentinit"
"queryorchestration/internal/serviceconfig/queue/documenttextprocess"
queuemock "queryorchestration/mocks/queue"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/pashagolub/pgxmock/v3"
"github.com/stretchr/testify/assert"
@@ -26,7 +24,6 @@ import (
type DocInitConfig struct {
runner.BaseConfig[S3EventNotification]
documentinit.DocInitConfig
documenttextprocess.DocTextProcessConfig
}
func TestDocInitRunner(t *testing.T) {
@@ -41,9 +38,8 @@ func TestDocInitRunner(t *testing.T) {
mockSQS := queuemock.NewMockSQSClient(t)
cfg.QueueClient = mockSQS
cfg.DocInitURL = "hi"
cfg.DocumentTextProcessURL = "hi"
runner := New(validator.New(), &Services{
runner := New(&Services{
documentstore.New(cfg),
})
assert.NotNil(t, runner)
+238 -182
View File
@@ -1,192 +1,248 @@
{
"Blocks": [
"base": [
{
"BlockType": "PAGE",
"ColumnIndex": null,
"ColumnSpan": null,
"Confidence": null,
"EntityTypes": null,
"Geometry": {
"BoundingBox": {
"Height": 1,
"Left": 0,
"Top": 0,
"Width": 1
},
"Polygon": [
{
"X": 0.0000010227642,
"Y": 0
},
{
"X": 1,
"Y": 0.0001285361
},
{
"X": 1,
"Y": 1
},
{
"X": 0,
"Y": 1
}
]
},
"Id": "cf623e5d-d3f1-4aa8-9255-970b0a178924",
"Page": null,
"Query": null,
"Relationships": [
"AnalyzeDocumentModelVersion": "1.0",
"Blocks": [
{
"Ids": [
"44a179f8-88e8-4cdd-aebf-96be77e681e0"
"BlockType": "PAGE",
"ColumnIndex": null,
"ColumnSpan": null,
"Confidence": null,
"EntityTypes": null,
"Geometry": {
"BoundingBox": {
"Height": 1,
"Left": 0,
"Top": 0,
"Width": 1
},
"Polygon": [
{
"X": 0.0000011078263,
"Y": 0
},
{
"X": 1,
"Y": 0.00008426185
},
{
"X": 1,
"Y": 1
},
{
"X": 0,
"Y": 1
}
]
},
"Id": "c811b895-fdd3-45b2-aaea-9b03ac08e6c7",
"Page": null,
"Query": null,
"Relationships": [
{
"Ids": [
"fb57ceba-2d5a-4334-bc58-5ae5c7cd1a82",
"fdc70cec-80dc-4833-81ac-ca919e28981e"
],
"Type": "CHILD"
}
],
"Type": "CHILD"
"RowIndex": null,
"RowSpan": null,
"SelectionStatus": "",
"Text": null,
"TextType": ""
},
{
"BlockType": "LINE",
"ColumnIndex": null,
"ColumnSpan": null,
"Confidence": 99.73413,
"EntityTypes": null,
"Geometry": {
"BoundingBox": {
"Height": 0.025445992,
"Left": 0.16460413,
"Top": 0.092252895,
"Width": 0.210229
},
"Polygon": [
{
"X": 0.16462424,
"Y": 0.092252895
},
{
"X": 0.3748331,
"Y": 0.092557475
},
{
"X": 0.37481487,
"Y": 0.117698886
},
{
"X": 0.16460413,
"Y": 0.11739321
}
]
},
"Id": "fb57ceba-2d5a-4334-bc58-5ae5c7cd1a82",
"Page": null,
"Query": null,
"Relationships": [
{
"Ids": [
"979fd840-9770-44c6-92ce-0414e9de1ce3",
"007d447f-af28-4535-8416-b8268650e578"
],
"Type": "CHILD"
}
],
"RowIndex": null,
"RowSpan": null,
"SelectionStatus": "",
"Text": "Hello World!",
"TextType": ""
},
{
"BlockType": "WORD",
"ColumnIndex": null,
"ColumnSpan": null,
"Confidence": 99.91676,
"EntityTypes": null,
"Geometry": {
"BoundingBox": {
"Height": 0.025081681,
"Left": 0.16460413,
"Top": 0.0924382,
"Width": 0.087125756
},
"Polygon": [
{
"X": 0.1646241,
"Y": 0.0924382
},
{
"X": 0.25172988,
"Y": 0.09256441
},
{
"X": 0.25171068,
"Y": 0.11751988
},
{
"X": 0.16460413,
"Y": 0.11739321
}
]
},
"Id": "979fd840-9770-44c6-92ce-0414e9de1ce3",
"Page": null,
"Query": null,
"Relationships": null,
"RowIndex": null,
"RowSpan": null,
"SelectionStatus": "",
"Text": "Hello",
"TextType": "PRINTED"
},
{
"BlockType": "WORD",
"ColumnIndex": null,
"ColumnSpan": null,
"Confidence": 99.55151,
"EntityTypes": null,
"Geometry": {
"BoundingBox": {
"Height": 0.024770401,
"Left": 0.26289847,
"Top": 0.09239531,
"Width": 0.11193464
},
"Polygon": [
{
"X": 0.2629173,
"Y": 0.09239531
},
{
"X": 0.3748331,
"Y": 0.092557475
},
{
"X": 0.37481526,
"Y": 0.117165715
},
{
"X": 0.26289847,
"Y": 0.11700299
}
]
},
"Id": "007d447f-af28-4535-8416-b8268650e578",
"Page": null,
"Query": null,
"Relationships": null,
"RowIndex": null,
"RowSpan": null,
"SelectionStatus": "",
"Text": "World!",
"TextType": "PRINTED"
},
{
"BlockType": "LAYOUT_TEXT",
"ColumnIndex": null,
"ColumnSpan": null,
"Confidence": 48.242188,
"EntityTypes": null,
"Geometry": {
"BoundingBox": {
"Height": 0.025445992,
"Left": 0.16460413,
"Top": 0.092252895,
"Width": 0.210229
},
"Polygon": [
{
"X": 0.16462424,
"Y": 0.092252895
},
{
"X": 0.3748331,
"Y": 0.092557475
},
{
"X": 0.37481487,
"Y": 0.117698886
},
{
"X": 0.16460413,
"Y": 0.11739321
}
]
},
"Id": "fdc70cec-80dc-4833-81ac-ca919e28981e",
"Page": null,
"Query": null,
"Relationships": [
{
"Ids": [
"fb57ceba-2d5a-4334-bc58-5ae5c7cd1a82"
],
"Type": "CHILD"
}
],
"RowIndex": null,
"RowSpan": null,
"SelectionStatus": "",
"Text": null,
"TextType": ""
}
],
"RowIndex": null,
"RowSpan": null,
"SelectionStatus": "",
"Text": null,
"TextType": ""
},
{
"BlockType": "LINE",
"ColumnIndex": null,
"ColumnSpan": null,
"Confidence": 99.89616,
"EntityTypes": null,
"Geometry": {
"BoundingBox": {
"Height": 0.024213256,
"Left": 0.16538842,
"Top": 0.092992365,
"Width": 0.20915127
},
"Polygon": [
{
"X": 0.165407,
"Y": 0.092992365
},
{
"X": 0.3745397,
"Y": 0.09330509
},
{
"X": 0.37452286,
"Y": 0.11720562
},
{
"X": 0.16538842,
"Y": 0.11689187
}
]
"DocumentMetadata": {
"Pages": 1
},
"Id": "44a179f8-88e8-4cdd-aebf-96be77e681e0",
"Page": null,
"Query": null,
"Relationships": [
{
"Ids": [
"b2eadea5-a686-48fe-9363-a774a0a146b4",
"b5939fe7-0f0d-4acf-8582-d45edc0669f8"
],
"Type": "CHILD"
}
],
"RowIndex": null,
"RowSpan": null,
"SelectionStatus": "",
"Text": "Hello World!",
"TextType": ""
},
{
"BlockType": "WORD",
"ColumnIndex": null,
"ColumnSpan": null,
"Confidence": 99.915146,
"EntityTypes": null,
"Geometry": {
"BoundingBox": {
"Height": 0.023926819,
"Left": 0.16538842,
"Top": 0.093094304,
"Width": 0.086175606
},
"Polygon": [
{
"X": 0.16540693,
"Y": 0.093094304
},
{
"X": 0.25156403,
"Y": 0.09322314
},
{
"X": 0.25154623,
"Y": 0.11702112
},
{
"X": 0.16538842,
"Y": 0.11689187
}
]
},
"Id": "b2eadea5-a686-48fe-9363-a774a0a146b4",
"Page": null,
"Query": null,
"Relationships": null,
"RowIndex": null,
"RowSpan": null,
"SelectionStatus": "",
"Text": "Hello",
"TextType": "PRINTED"
},
{
"BlockType": "WORD",
"ColumnIndex": null,
"ColumnSpan": null,
"Confidence": 99.87717,
"EntityTypes": null,
"Geometry": {
"BoundingBox": {
"Height": 0.02385673,
"Left": 0.26209888,
"Top": 0.09313698,
"Width": 0.1124408
},
"Polygon": [
{
"X": 0.2621165,
"Y": 0.09313698
},
{
"X": 0.3745397,
"Y": 0.09330509
},
{
"X": 0.374523,
"Y": 0.1169937
},
{
"X": 0.26209888,
"Y": 0.116825044
}
]
},
"Id": "b5939fe7-0f0d-4acf-8582-d45edc0669f8",
"Page": null,
"Query": null,
"Relationships": null,
"RowIndex": null,
"RowSpan": null,
"SelectionStatus": "",
"Text": "World!",
"TextType": "PRINTED"
"HumanLoopActivationOutput": null,
"ResultMetadata": {}
}
],
"DetectDocumentTextModelVersion": "1.0",
"DocumentMetadata": {
"Pages": 1
},
"ResultMetadata": {}
"table": []
}
+70 -74
View File
@@ -1,134 +1,130 @@
Document Index
AMENDMENT NUMBER ONE 1PARTICIPATING PROVIDER AGREEMENT 1
Attachment A: Medicaid 3
EXHIBIT 2 3COMPENSATION SCHEDULE 3PROFESSIONAL SERVICES 3
Additional Provisions: 3
Definitions: 4
Start of Page No. = 1
AMENDMENT NUMBER ONE
PARTICIPATING PROVIDER AGREEMENT
This Amendment Number One ("Amendment") is entered into as of December 1. 2019 by and between Dummy
HealthCare Inc. ("Health Plan") and Picture, LLC Novelty Pharmacy
("Provider"), collectively reterred to
HealthCare Inc. ("Health Plan") and Picture. LLC Novelty Pharmacy
("Provider"). collectively referred to
herein as the "Parties".
WHEREAS, Health Plan and Provider have previously entered into a Participating Provider Agreement (the
"Agreement") effective as of January 1. 2017 (defined in the Agreement as the "Effective Date"); and
WITEREAS, the Parties desire to amend the Agreement;
NOW THEREFORE, in consideration of the promises and mutual covenants herein contained. the Parties agree as
"Agreement") effective as of January 1. 2017 (defined in the Agreement as the "Effective Date"). and
WHEREAS. the Parties desire to amend the Agreement:
NOW THEREFORE in consideration of the promises and mutual covenants herein contained the Parties agree as
follows:
1.
Attachment A: Medicaid Exhibit 2 Compensation Schedule Professional Services shall be added
and
incorporated into the Agreement as shown by the attached.
2. All other terms and conditions of the Agreement and any amendments thereto, if any, shall remain in full
force and effect. If the terms of this Amendment conflict with any of the terms of the Agreement. the terms
I. Attachment A: Medicaid Exhibit 2 Compensation Schedule Professional Services shall be added and
incorporated into the Agreement as shown by the attached
2. All other terms and conditions of the Agreement and any amendments thereto. if any. shall remain in full
force and effect If the terms of this Amendment conflict with any of the terms of the Agreement. the terms
of this Amendment shall prevail.
PPA (NE) - All Products 03/30/16
Page I of 4
PPA (NF All Products 03/30
Page of 4
Start of Page No. = 2
IN WITNESS WHEREOF. the Parties hereto have executed and delivered this
IN WITNESS WHEREOF the Parties hereto have executed and delivered this
Amendment as of the date first set forth above.
HEALTH PLAN:
PROVIDER:
Dummy HealthCare Inc.
Authorization
Benth
PROVIDER
LLC Novelty Pharmacy
Authorized Signature
Authorized Signature
Bank
Mark Chappman
Jake Parelta
Printed Name:
Printed Name:
Title: PRocedent
"Printed Name:
Title: RPinCharge
Date: 11/19/9
Date: 11/15/19
ECM #: 112233
Tax ID Number: 12-3456789
Tax ID Number 12-3456789
State Medicaid Number: 0123456789
National Provider Identifier: 123456789
Mark Chappman
Printed Name:
Title: PRocedent
Date: 11/19/19
ECM #: 112233
PPA (NE) - All Products 03/30/16
Page 2 of 4
Page of 4
This page has 2 signature.
Start of Page No. = 3
Attachment A: Medicaid
EXHIBIT 2
COMPENSATION SCHEDULE
PROFESSIONAL SERVICES
Picture,
LLC Novelty Pharmacy
Picture, LLC Novelty Pharmacy
This compensation schedule ("Compensation Schedule") sets forth the maximum reimbursement amounts for
Covered Services provided by Contracted Providers to Covered Persons enrolled in a Medicaid Product. Where the
Covered Services provided by Contracted Providers to Covered Persons enrolled in a Medicaid Product Where the
Contracted Provider's tax identification number ("TIN") has been designated by the Payor as subject to this
Compensation Schedule. Payor shall pay or arrange for payment of a Clean Claim for Covered Services rendered by
Compensation Schedule Payor shall pay or arrange for payment of a Clean Claim for Covered Services rendered by
the Contracted Provider according to the terms of, and subject to the requirements set forth in, the Agreement and
this Compensation Schedule. Payment under this Compensation Schedule shall consist of the Allowed Amount as set
forth herein less all applicable Cost-Sharing Amounts. All capitalized terms used in this Compensation Schedule
shall have the meanings set forth in the Agreement, the applicable Product Attachment. or the Definitions section set
this Compensation Schedule Payment under this Compensation Schedule shall consist of the Allowed Amount as set
forth herein less all applicable Cost-Sharing Amounts All capitalized terms used in this Compensation Schedule
shall have the meanings set forth in the Agreement. the applicable Product Attachment or the Definitions section set
forth at the end of this Compensation Schedule.
The maximum compensation for professional Covered Services rendered to a Covered Person shall be the "Allowed
Amount." Except as otherwise provided in this Compensation Schedule. the Allowed Amount for professional
Covered Services is the lesser of: (i) Allowable Charges; or (ii) one hundred percent (100%) of the Payor's Medicaid
fee schedule.
fee schedule
Additional Provisions:
1.
Code Change Updates. Payor utilizes nationally recognized coding structures (including. without limitation,
revenue codes, CPT codes, HCPCS codes, ICD codes, national drug codes, ASA relative values, etc., or their
successors) for basic coding and descriptions of the services rendered. Updates to billing-related codes shall
Code Change Updates Payor utilizes nationally recognized coding structures (including without limitation,
revenue codes CPT codes. HCPCS codes, ICD codes. national drug codes. ASA relative values etc., or their
successors) for basic coding and descriptions of the services rendered Updates to billing-related codes shall
become effective on the date ("Code Change Effective Date") that is the later of: (i) the first day of the month
following sixty (60) days after publication by the governmental agency having authority over the applicable
Product of such governmental agency's acceptance of such code updates, (ii) the effective date of such code
Product of such governmental agency's acceptance of such code updates. (ii) the effective date of such code
updates as determined by such governmental agency or (iii) if a date is not established by such governmental
agency or the applicable Product is not regulated by such governmental agency. the date that changes are made
to nationally recognized codes. Such updates may include changes to service groupings. Claims processed prior
to the Code Change Effective Date shall not be reprocessed to reflect any such code updates.
2.
Fee Change Updates. Updates to the fee schedule shall become effective on the effective date of such fee
Fee Change Updates Updates to the fee schedule shall become effective on the effective date of such fee
schedule updates. as determined by the Payor ("Fee Change Effective Date"). The date of implementation of any
fee schedule updates. i.e. the date on which such fee change is first used for reimbursement ("Fee Change
Implementation Date"). shall be the later of: (i) the first date on which Payor is reasonably able to implement the
update in the claims payment system; or (ii) the Fee Change Effective Date. Claims processed prior to the Fee
Change implementation Date shall not be reprocessed to reflect any updates to such fee schedule. even if service
update in the claims payment system; or (ii) the Fee Change Effective Date Claims processed prior to the Fee
Change Implementation Date shall not be reprocessed to reflect any updates to such fee schedule. even if service
was provided after the Fee Change Effective Date.
3.
Modifier, Unless specifically indicated otherwise, fee amounts listed in the fee schedule represent global fees
and may be subject to reductions based on appropriate Modifier (for example, professional and technical
modifiers). As used in the previous sentence, "global fees" refers to services billed without a Modifier, for which
3. Modifier Unless specifically indicated otherwise fee amounts listed in the fee schedule represent global fees
and may be subject to reductions based on appropriate Modifier (for example. professional and technical
modifiers) As used in the previous contence. "global fees" refers to services billed without a Modifier. for which
the fee amount includes both the professional component and the technical component. Any Cost-Sharing
Amounts that the Covered Person is responsible to pay under the Coverage Agreement will be subtracted from
the Allowed Amount in determining the amount to be paid.
4.
Anesthesia Modifier Pricing Rules. The dollar amount that will be used in the calculation of time-based and non-
4. Anesthesia Modifier Pricing Rules, The dollar amount that will be used in the calculation of time-based and non-
time based anesthesia management fees in accordance with the anesthesia payment policy. Unless specifically
PPA (NE) - All Products 03/30/16
Page 3 of 4
PPA (NE) All Products 03/30/16
Page of 4
Start of Page No. = 4
stated otherwise, the anesthesia conversion factor indicated is fixed and will not change. The anesthesia
conversion factor is based on an anesthesia time unit value of 15 minutes.
5. Payment for Multiple Procedures. Where multiple outpatient surgical or scope procedures performed on a
Covered Person during a single occasion of surgery, reimbursement will be as follows: i) the procedure for which
stated otherwise. the anesthesia conversion factor indicated is fixed and will not change. The anesthesia
conversion factor is based on an anesthesia time unit value of 15 minutes
5. Payment for Multiple Procedures Where multiple outpatient surgical or scope procedures performed on a
Covered Person during a single occasion of surgery. reimbursement will be as follows: i) the procedure for which
the Allowed Amount under this Compensation Schedule is greatest will be reimbursed at one hundred percent
(100%) of such Allowed Amount; and ii) the other procedures under this Compensation Schedule will each be
reimbursed at fifty percent (50%) of such Allowed Amounts..
6. Place of Service Pricing Rules. This fee schedule follows CMS guidelines for determining when services are
(100%) of such Allowed Amount: and ii) the other procedures under this Compensation Schedule will each be
reimbursed at fifty percent (50%) of such Allowed Amounts.
6
Place of Service Pricing Rules This fee schedule follows CMS guidelines for determining when services are
priced at the facility or non-facility fee schedule (with the exception of services performed at Ambulatory Surgery
Centers. POS 24, which will be priced at the facility fee schedule).
7. Payment under this Compensation Schedule. All payments under this Compensation Schedule are subject to the
terms and conditions set forth in the Agreement. the Provider Manual and any applicable billing manual.
Centers. POS 24. which will be priced at the facility fee schedule).
7. Payment under this Compensation Schedule All payments under this Compensation Schedule are subject to the
terms and conditions set forth in the Agreement. the Provider Manual and any applicable billing manual
Definitions:
1. Allowed Amount means the amount designated in this Compensation Schedule as the maximum amount payable
to a Contracted Provider for any particular Covered Service provided to any particular Covered Person, pursuant
to this Agreement or its Attachments.
2. Allowable Charges means a Contracted Provider's billed charges for services that qualify as Covered Services.
3. Cost-Sharing Amounts means any amounts payable by a Covered Person. such as copayments. cost-sharing,
coinsurance, deductibles or other amounts that are the Covered Person's financial responsibility under the
applicable Coverage Agreement. if applicable.
PPA (NE) - All Products 03/30/16
Page 4 of 4
to this Agreement or its Attachments
2. Allowable Charges means a Contracted Provider's billed charges for services that qualify as Covered Services
3. Cost-Sharing Amounts means any amounts payable by a Covered Person. such as copayments. cost-sharing
coinsurance deductibles or other amounts that are the Covered Person's financial responsibility under the
applicable Coverage Agreement. if applicable
PPA (NE All Products 03/30/16
Page of 4
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+45 -56
View File
@@ -1,46 +1,35 @@
Document Index
EXHIBIT A-1 2
COMMERCIAL BENEFIT PROGRAMS 2
DIRECT NETWORK FEE-FOR-SERVICE 2RATE EXHIBIT 2
MEDICARE ADVANTAGE PROGRAM 3DIRECT NETWORK FEE-FOR-SERVICE 3RATE EXHIBIT 3
I. Payment Rates 3
MEDI-CAL BENEFIT PROGRAM 4DIRECT NETWORK FEE-FOR-SERVICE 4RATE EXHIBIT 4
Start of Page No. = 1
7.14
Status as Independent Entities. None of the provisions of this Agreement is intended to create,
nor
shall be deemed or construed to create any relationship between Provider and All Health or a Payor other than
7.14 Status as Independent Entities. None of the provisions of this Agreement is intended to create,
nor shall be deemed or construed to create any relationship between Provider and All Health or a Payor other than
that of independent entities contracting with each other solely for the purpose of effecting the provisions of this
Agreement. Neither Provider nor All Health /Payor, nor any of their respective agents, employees or representatives
shall be construed to be the agent, employee or representative of the other.
Agreement Neither Provider nor All Health /Payor. nor any of their respective agents, employees or representatives
shall be construed to be the agent employee or representative of the other
7.15
Addenda. Each Addendum to this Agreement is made a part of this Agreement as though set
forth fully herein. Any provision of an Addendum that is in conflict with any provision of this Agreement shall take
Addenda Each Addendum to this Agreement is made a part of this Agreement as though set
forth fully herein Any provision of an Addendum that is in conflict with any provision of this Agreement shall take
precedence and supersede the conflicting provision of this Agreement with respect to the subject matter of the
Addendum.
7.16
Calculation of Time. The parties agree that for purposes of calculating time under this
Agreement, any time period of less than ten (10) days shall be deemed to refer to business days and any time period
Addendum
7.16 Calculation of Time. The parties agree that for purposes of calculating time under this
Agreement any time period of less than ten (10) days shall be deemed to refer to business days and any time period
of ten (10) days or more shall be deemed to refer to calendar days unless the term "business" precedes the term
"days".
"days"
7.17
Waiver of Breach. The waiver of any breach of this Agreement by either party shall not
Waiver of Breach The waiver of any breach of this Agreement by either party shall not
constitute a continuing waiver of any subsequent breach of either the same or any other provision(s) of this
Agreement. Further, any such waiver shall not be construed to be a waiver on the part of such party to enforce strict
compliance in the future and to exercise any right or remedy related thereto.
Agreement Further, any such waiver shall not be construed to be a waiver on the part of such party to enforce strict
compliance in the future and to exercise any right or remedy related thereto
THIS CONTRACT CONTAINS A BINDING ARBITRATION CLAUSE, WHICH MAY BE ENFORCED
BY THE PARTIES.
IN WITNESS WHEREOF, the parties have executed this Agreement.
BY THE PARTIES
IN WITNESS WHEREOF the parties have executed this Agreement
PROVIDER
ALL HEALTH
Smoke
Smith
Signature
Health Net Signature
Peanth
Harvey
DUONG M.S.
DUONG MS
Steve Carrol
Print Name
Print Name
@@ -48,7 +37,7 @@ MEDICAL DIRECTOR
VP Provider Network Management & Strategy
Title
Title
Dummy Group Name, Inc.
Dummy Group Name Inc.
Group Name (If Applicable)
Date
3/28/2011
@@ -59,34 +48,34 @@ Effective Date
2/2/2011
Date
California Provider Participation Agreement
18
Fee-For-Service Direct Network Template
For-Service Direct Network Template
HN-DN-PPA-11-03-2010
18
This page has 2 signature.
Start of Page No. = 2
EXHIBIT A-1
COMMERCIAL BENEFIT PROGRAMS
DIRECT NETWORK FEE-FOR-SERVICE
RATE EXHIBIT
Subject to the terms of this Agreement, including without limitation the Payment Conditions set forth in Addendum E,
Subject to the terms of this Agreement, including without limitation the Payment Conditions set forth in Addendum E.
All Health or Payor shall pay and Provider shall accept as payment in full for non-capitated Medically Necessary
Covered Services delivered under commercial Benefit Programs pursuant to this Addendum, the lesser of: (i) the rates
listed below, or (ii) 100% of Provider's billed charges.
Covered Services delivered under commercial Benefit Programs pursuant to this Addendum, the lesser of (i) the rates
listed below. or (ii) 100% of Provider's billed charges.
-------Table Start--------
[["Category of Service","Compensation"],["Covered Services delivered or arranged by Provider. excluding Laboratory services","95% of CMS Allowable"],["Anesthesia Services when provided by an Anesthesiologist or Certified Registered Nurse Anesthetist (American Society of Anesthesiology (ASA) unit scale)","$39 ASA unit"],["Medical/Surgical Services by an Anesthesiologist or Certified Registered Nurse Anesthetist","95% of CMS Allowable"],["Laboratory Services performed in Provider or Professional Provider office","95% of CMS Allowable"],["Pharmaceuticals","95% of the Average Wholesale Price (AWP)"],["OB Services - CPT 59400: Global Obstetric care with vaginal delivery - CPT 59510: Global Obstetric care with cesarean delivery - CPT 59610: Vaginal Delivery after previous cesarean delivery - CPT 59618: Attempted vaginal delivery, resulting in cesarean","$1,700.00 $1,700.00 $1,700.00 $1,700.00"],["Immunizations","95% of the Average Wholesale Price (AWP) as determined by Health Net."],["General Health Panel - CPT 80050: General Health Panel CPT 80055: Obstetric Panel","$20.00 $15.00"],["By Report (BR) Procedures, Procedures not Listed and Procedures with Relativities not Established","75% of billed charges for Covered Services"]]
-------Table End--------
California Provider Participation Agreement
22
Fee-For-Service Direct Network Template
HN-DN-PPA-11-03-2010
22
-------Table Start--------
7436be95-25d1-41ad-9abb-1d9e85b85440
[['Category of Service', 'Compensation'], ['Covered Services delivered or arranged by Provider, excluding Laboratory services', '95% of CMS Allowable'], ['Anesthesia Services when provided by an Anesthesiologist or Certified Registered Nurse Anesthetist (American Society of Anesthesiology (ASA) unit scale)', '$39 / ASA unit'], ['Medical/Surgical Services by an Anesthesiologist or Certified Registered Nurse Anesthetist', '95% of CMS Allowable'], ['Laboratory Services performed in Provider or Professional Provider office', '95% of CMS Allowable'], ['Pharmaceuticals', '95% of the Average Wholesale Price (AWP)'], ['OB Services - CPT 59400: Global Obstetric care with vaginal delivery - CPT 59510: Global Obstetric care with cesarean delivery - CPT 59610: Vaginal Delivery after previous cesarean delivery - CPT 59618: Attempted vaginal delivery, resulting in cesarean', '$1,700.00 $1,700.00 $1,700.00 $1,700.00'], ['Immunizations', '95% of the Average Wholesale Price (AWP) as determined by Health Net.'], ['General Health Panel - CPT 80050: General Health Panel - CPT 80055: Obstetric Panel', '$20.00 $15.00'], ['By Report (BR) Procedures, Procedures not Listed and Procedures with Relativities not Established', '75% of billed charges for Covered Services']]
DIRECT NETWORK FEE-FOR-SERVICE RATE EXHIBIT
-------Table End--------
Start of Page No. = 3
EXHIBIT B-1
MEDICARE ADVANTAGE PROGRAM
DIRECT NETWORK FEE-FOR-SERVICE
@@ -95,20 +84,18 @@ I. Payment Rates
Subject to the terms of this Agreement, including without limitation the Payment Conditions set forth in Addendum E,
All Health shall pay and Provider shall accept as payment in full for non-capitated Medically Necessary Covered
Services delivered under Medicare Advantage Benefit Programs pursuant to this Addendum, the lesser of: (i) the rates
listed below, or (ii) 100% of Provider's billed charges.
California Provider Participation Agreement
27
Fee-For-Service Direct Network Template
HN-DN-PPA-11-03-2010
listed below, or (ii) 100% of Provider's billed charges
-------Table Start--------
[['Category of Service', 'Compensation'], ['Covered Services delivered or arranged by Provider', '100% of CMS Allowable'], ['General Health Panel - CPT 80050: General Health Panel - CPT 80055: Obstetric Panel', '$20.00 $15.00'], ['By Report (BR) Procedures, Procedures not Listed and Procedures with Relativities not Established', '75% of billed charges for Covered Services']]
None
[["Category of Service","Compensation"],["Covered Services delivered or arranged by Provider","100% of CMS Allowable"],["General Health Panel",""],["- CPT 80050 General Health Panel","$20.00"],["- CPT 80055: Obstetric Panel","$15.00"],["By Report (BR) Procedures. Procedures not Listed and Procedures with Relativities not Established","75% of billed charges for Covered Services"]]
-------Table End--------
California Provider Participation Agreement
Fee-Fer-Service Direct Network Template
HN-DN-PPA-11-03-2010
27
Start of Page No. = 4
EXHIBIT C-1
MEDI-CAL BENEFIT PROGRAM
DIRECT NETWORK FEE-FOR-SERVICE
@@ -118,8 +105,10 @@ All Health shall pay and Provider shall accept as payment in full for non-capita
Services delivered pursuant to this Addendum, the lesser of: 100% of the State of California Medi-Cal Fee Schedule
rates in effect at the time of service, subject to any adjustments made by the State of California under the applicable
Medi-Cal Fee-For-Service Program; (ii) Fee-for-service rates for the commercial Benefit Program set forth in
Addendum A, Exhibit A-1; or (iii) Provider's billed charges.
Addendum A. Exhibit A-1: or (iii) Provider's billed charges.
California Provider Participation Agreement
34
Fee-For-Service Direct Network Template
HN-DN-PPA-11-03-2010
HN-DN-PPA-11-03-2010
34
+18 -6
View File
@@ -6,18 +6,30 @@ FROM golang:${GO_VERSION} AS build
WORKDIR /app
# Install Git if not already present
RUN apt-get update && apt-get install -y --no-install-recommends git=1:2.* && apt-get clean && rm -rf /var/lib/apt/lists/*
RUN apt-get update && \
apt-get install -y --no-install-recommends git=1:2.* musl-tools=1.2.* \
&& apt-get clean && \
\rm -rf /var/lib/apt-get/lists/*
# Copy the entire repository including .git directory
COPY . .
RUN go mod verify
ARG TARGETARCH
SHELL ["/bin/bash", "-c"]
ENV CGO_ENABLED=1
ENV GOARCH=${TARGETARCH}
ENV CC=musl-gcc
# Must be same a github.com/gen2brain/go-fitz in go.mod
# ENV FZ_VERSION=1.24.14
RUN --mount=type=cache,target=/go/pkg/mod/ \
CGO_ENABLED=0 GOARCH=$TARGETARCH go build -o /bin ./cmd/...
go build \
-mod vendor \
-tags musl \
-ldflags "-linkmode external -extldflags -static" \
-o /bin ./cmd/...
FROM scratch AS final
+1 -1
View File
@@ -31,7 +31,7 @@ func main() {
cfg.ControllerFunc = func() runner.Controller[clientsyncrunner.Body] {
svc := clientsync.New(cfg)
c := clientsyncrunner.New(cfg.GetValidator(), &clientsyncrunner.Services{
c := clientsyncrunner.New(&clientsyncrunner.Services{
ClientSync: svc,
})
+1 -1
View File
@@ -35,7 +35,7 @@ func main() {
cfg.ControllerFunc = func() runner.Controller[doccleanrunner.Body] {
clean := documentclean.New(cfg)
return doccleanrunner.New(cfg.GetValidator(), &doccleanrunner.Services{
return doccleanrunner.New(&doccleanrunner.Services{
Clean: clean,
})
}
+1 -1
View File
@@ -41,7 +41,7 @@ func main() {
cfg.ControllerFunc = func() runner.Controller[docinitrunner.Body] {
docinit := documentinit.New(cfg)
return docinitrunner.New(cfg.GetValidator(), &docinitrunner.Services{
return docinitrunner.New(&docinitrunner.Services{
Document: docinit,
})
}
+1 -1
View File
@@ -42,7 +42,7 @@ func main() {
Client: cli,
})
return docsyncrunner.New(cfg.GetValidator(), &docsyncrunner.Services{
return docsyncrunner.New(&docsyncrunner.Services{
Document: docsync,
})
}
-66
View File
@@ -1,66 +0,0 @@
// **Listens For**: Body Containing Textract Result Document Metadata
//
// **Action**.
//
// Process textract output.
//
// An event with the document id is pushed to the QUERYSYNC queue.
package main
import (
"context"
"log/slog"
"os"
doctextprocessrunner "queryorchestration/api/docTextProcessRunner"
documenttext "queryorchestration/internal/document/text"
"queryorchestration/internal/server/runner"
"queryorchestration/internal/serviceconfig/aws"
"queryorchestration/internal/serviceconfig/objectstore"
"queryorchestration/internal/serviceconfig/queue/querysync"
"queryorchestration/internal/serviceconfig/textract"
_ "github.com/lib/pq"
)
type DocTextProcessConfig struct {
runner.BaseConfig[doctextprocessrunner.Body]
aws.AWSConfig
textract.TextractConfig
querysync.QuerySyncConfig
objectstore.ObjectStoreConfig
}
func main() {
ctx := context.Background()
cfg := &DocTextProcessConfig{}
cfg.ControllerFunc = func() runner.Controller[doctextprocessrunner.Body] {
text := documenttext.New(cfg)
return doctextprocessrunner.New(cfg.GetValidator(), &doctextprocessrunner.Services{
Text: text,
})
}
server, err := runner.New(ctx, cfg)
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
err = cfg.SetStoreClient(ctx)
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
err = cfg.SetTextractClient(ctx)
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
server.Listen(ctx)
}
+7 -1
View File
@@ -37,7 +37,7 @@ func main() {
cfg.ControllerFunc = func() runner.Controller[doctextrunner.Body] {
text := documenttext.New(cfg)
return doctextrunner.New(cfg.GetValidator(), &doctextrunner.Services{
return doctextrunner.New(&doctextrunner.Services{
Text: text,
})
}
@@ -48,6 +48,12 @@ func main() {
os.Exit(1)
}
err = cfg.SetStoreClient(ctx)
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
err = cfg.SetTextractClient(ctx)
if err != nil {
slog.Error(err.Error())
+1 -1
View File
@@ -45,7 +45,7 @@ func main() {
Sync: sync,
})
c := queryrunner.New(cfg.GetValidator(), &queryrunner.Services{
c := queryrunner.New(&queryrunner.Services{
ResultSet: resset,
})
+1 -1
View File
@@ -45,7 +45,7 @@ func main() {
ResultSync: sync,
})
c := querysyncrunner.New(cfg.GetValidator(), &querysyncrunner.Services{
c := querysyncrunner.New(&querysyncrunner.Services{
QuerySync: svc,
})
+1 -1
View File
@@ -31,7 +31,7 @@ func main() {
cfg.ControllerFunc = func() runner.Controller[queryversionsyncrunner.Body] {
svc := queryversionsync.New(cfg)
c := queryversionsyncrunner.New(cfg.GetValidator(), &queryversionsyncrunner.Services{
c := queryversionsyncrunner.New(&queryversionsyncrunner.Services{
Sync: svc,
})
+2 -4
View File
@@ -4,7 +4,7 @@
//
// It will identify the type of event and the location of said event, and forward the task accordingly.
//
// It supports the document initialisation and document text processing stages. Events may be pushed to the DOCINIT and DOCTEXTPROCESS queues.
// It supports the document initialisation and document text processing stages. Events may be pushed to the DOCINIT queue.
package main
import (
@@ -16,7 +16,6 @@ import (
documentstore "queryorchestration/internal/document/store"
"queryorchestration/internal/server/runner"
"queryorchestration/internal/serviceconfig/queue/documentinit"
"queryorchestration/internal/serviceconfig/queue/documenttextprocess"
_ "github.com/lib/pq"
)
@@ -24,7 +23,6 @@ import (
type StoreEventConfig struct {
runner.BaseConfig[storeeventrunner.S3EventNotification]
documentinit.DocInitConfig
documenttextprocess.DocTextProcessConfig
}
func main() {
@@ -35,7 +33,7 @@ func main() {
cfg.ControllerFunc = func() runner.Controller[storeeventrunner.S3EventNotification] {
store := documentstore.New(cfg)
return storeeventrunner.New(cfg.GetValidator(), &storeeventrunner.Services{
return storeeventrunner.New(&storeeventrunner.Services{
Store: store,
})
}
+110 -17
View File
@@ -9,17 +9,40 @@ import (
"fmt"
"log/slog"
"os"
documenttext "queryorchestration/internal/document/text"
documenttypes "queryorchestration/internal/document/types"
"queryorchestration/internal/serviceconfig"
"queryorchestration/internal/serviceconfig/aws"
"queryorchestration/internal/serviceconfig/objectstore"
"queryorchestration/internal/serviceconfig/queue/querysync"
"queryorchestration/internal/serviceconfig/textract"
awstextract "github.com/aws/aws-sdk-go-v2/service/textract"
"github.com/aws/aws-sdk-go-v2/service/textract/types"
)
type Config struct {
serviceconfig.BaseConfig
textract.TextractConfig
querysync.QuerySyncConfig
objectstore.ObjectStoreConfig
}
func main() {
ctx := context.Background()
cfg := &textract.TextractConfig{}
if len(os.Args) != 3 {
fmt.Println("Error: expected at least two arguments")
fmt.Println("Usage:", os.Args[0], "<sample_name>", "<file_name>")
os.Exit(1)
}
sampleName := os.Args[1]
name := os.Args[2]
slog.Info("arguments", "sample_name", sampleName, "file_name", name)
cfg := &Config{}
profile := aws.Profile(os.Getenv("AWS_PROFILE"))
if profile == "" {
slog.Error("profile not available")
@@ -32,35 +55,105 @@ func main() {
os.Exit(1)
}
base := "../../assets/sampleGeneration"
name := "helloWorld"
filename := fmt.Sprintf("%s/%s.pdf", base, name)
content, err := os.ReadFile(filename)
root, err := os.OpenRoot("assets")
if err != nil {
slog.Error("error writing file", "error", err)
slog.Error("error opening root", "error", err)
os.Exit(1)
}
slog.Info("detecting text", "filename", filename)
res, err := cfg.GetTextractClient().DetectDocumentText(ctx, &awstextract.DetectDocumentTextInput{
Document: &types.Document{
Bytes: content,
},
})
root, err = root.OpenRoot(sampleName)
if err != nil {
slog.Error("error detecting text", "error", err)
slog.Error("error opening sample", "error", err)
os.Exit(1)
}
jsonContent, err := json.MarshalIndent(res, "", "\t")
pdfFile, err := root.Open(fmt.Sprintf("%s.pdf", name))
if err != nil {
slog.Error("error opening pdf file", "error", err)
os.Exit(1)
}
pdf, err := documenttypes.NewPDFFromReader(pdfFile)
if err != nil {
slog.Error("error creating pdf file", "error", err)
os.Exit(1)
}
textSvc := documenttext.New(cfg)
count, err := pdf.GetPageCount(ctx)
if err != nil {
slog.Error("error getting page count", "error", err)
os.Exit(1)
}
slog.Info("detecting text")
allResults := map[string][]awstextract.AnalyzeDocumentOutput{}
allResults["base"] = make([]awstextract.AnalyzeDocumentOutput, count)
allResults["table"] = []awstextract.AnalyzeDocumentOutput{}
for i := range count {
result, err := textSvc.GetBasePage(ctx, pdf, i)
if err != nil {
slog.Error("error detecting text", "error", err)
os.Exit(1)
}
for _, block := range result.Blocks {
feature := textSvc.GetBlockFeature(block)
if feature == documenttext.FeatureTable {
pageBytes, err := pdf.GetPageAsPNG(ctx, i)
if err != nil {
slog.Error("error getting page", "error", err)
os.Exit(1)
}
res, err := cfg.GetTextractClient().AnalyzeDocument(ctx, &awstextract.AnalyzeDocumentInput{
Document: &types.Document{
Bytes: pageBytes,
},
FeatureTypes: []types.FeatureType{
types.FeatureTypeTables,
},
})
if err != nil {
slog.Error("error detecting table", "error", err)
os.Exit(1)
}
allResults["table"] = append(allResults["table"], *res)
}
}
allResults["base"][i] = result
}
generatedBase := "generated"
if _, err := root.Stat(generatedBase); os.IsNotExist(err) {
err := root.Mkdir(generatedBase, 0755)
if err != nil {
slog.Error("error creating directory", "error", err)
os.Exit(1)
}
slog.Info("directory created", "directory", generatedBase)
}
jsonContent, err := json.MarshalIndent(allResults, "", "\t")
if err != nil {
slog.Error("error marshaling json", "error", err)
os.Exit(1)
}
outputFilename := fmt.Sprintf("%s/generated/%s.gen", base, name)
err = os.WriteFile(outputFilename, jsonContent, 0600)
outputFilename := fmt.Sprintf("%s/%s.gen", generatedBase, name)
file, err := root.Create(outputFilename)
if err != nil {
slog.Error("error creating file", "error", err)
os.Exit(1)
}
defer file.Close()
_, err = file.Write(jsonContent)
if err != nil {
slog.Error("error writing file", "error", err)
os.Exit(1)
-31
View File
@@ -156,37 +156,6 @@ services:
networks:
- server-network
doc_text_process_runner:
image: queryorchestration:latest
command: ["./docTextProcessRunner"]
depends_on:
localstack:
condition: service_healthy
db:
condition: service_healthy
ports:
- "8086:8080"
expose:
- 8080
environment:
LOG_LEVEL: DEBUG
QUEUE_URL: ${DOCUMENT_TEXT_PROCESS_URL}
QUERY_SYNC_URL: ${QUERY_SYNC_URL}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
AWS_SESSION_TOKEN: ${AWS_SESSION_TOKEN}
AWS_REGION: ${AWS_REGION}
PGUSER: ${PGUSER}
PGPASSWORD: ${PGPASSWORD}
PGHOST: db
PGPORT: 5432
PGDATABASE: ${PGDATABASE}
DB_NOSSL: ${DB_NOSSL}
AWS_ENDPOINT_URL: "http://localstack:4566"
AWS_S3_USE_PATH_STYLE: true
networks:
- server-network
query_sync_runner:
image: queryorchestration:latest
command: ["./querySyncRunner"]
+1 -11
View File
@@ -48,16 +48,6 @@ By validating: the client can_sync parameter.
If eligible, send an event to the DOCCLEAN queue.
## docTextProcessRunner
**Listens For**: Body Containing Textract Result Document Metadata
**Action**.
Process textract output.
An event with the document id is pushed to the QUERYSYNC queue.
## docTextRunner
**Listens For**: Body Containing Document ID
@@ -144,6 +134,6 @@ Adds an event for each client into the CLIENTSYNC queue for the given query, i.e
It will identify the type of event and the location of said event, and forward the task accordingly.
It supports the document initialisation and document text processing stages. Events may be pushed to the DOCINIT and DOCTEXTPROCESS queues.
It supports the document initialisation and document text processing stages. Events may be pushed to the DOCINIT queue.
Generated by Doczy
+7 -1
View File
@@ -9,9 +9,11 @@ require (
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.1
github.com/aws/aws-sdk-go-v2/service/textract v1.35.1
github.com/caarlos0/env/v11 v11.3.1
github.com/gen2brain/go-fitz v1.24.14
github.com/getkin/kin-openapi v0.129.0
github.com/go-playground/validator/v10 v10.25.0
github.com/golang-migrate/migrate/v4 v4.18.2
github.com/google/go-cmp v0.7.0
github.com/jackc/pgx/v5 v5.7.2
github.com/joho/godotenv v1.5.1
github.com/labstack/echo-contrib v0.17.2
@@ -40,6 +42,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chigopher/pathlib v0.19.1 // indirect
github.com/ebitengine/purego v0.8.0 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
@@ -64,11 +67,13 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jupiterrider/ffi v0.2.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
@@ -81,6 +86,7 @@ require (
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/zerolog v1.33.0 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
@@ -140,7 +146,7 @@ require (
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.0.1+incompatible // indirect
github.com/docker/docker v28.0.1+incompatible
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
+11
View File
@@ -86,6 +86,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@@ -94,6 +96,8 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gen2brain/go-fitz v1.24.14 h1:09weRkjVtLYNGo7l0J7DyOwBExbwi8SJ9h8YPhw9WEo=
github.com/gen2brain/go-fitz v1.24.14/go.mod h1:0KaZeQgASc20Yp5R/pFzyy7SmP01XcoHKNF842U2/S4=
github.com/getkin/kin-openapi v0.129.0 h1:QGYTNcmyP5X0AtFQ2Dkou9DGBJsUETeLH9rFrJXZh30=
github.com/getkin/kin-openapi v0.129.0/go.mod h1:gmWI+b/J45xqpyK5wJmRRZse5wefA5H0RDMK46kLUtI=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
@@ -171,6 +175,8 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/jupiterrider/ffi v0.2.0 h1:tMM70PexgYNmV+WyaYhJgCvQAvtTCs3wXeILPutihnA=
github.com/jupiterrider/ffi v0.2.0/go.mod h1:yqYqX5DdEccAsHeMn+6owkoI2llBLySVAF8dwCDZPVs=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@@ -204,6 +210,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@@ -261,6 +269,9 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+31 -18
View File
@@ -7,7 +7,6 @@ import (
"fmt"
"log/slog"
"queryorchestration/internal/serviceconfig"
"queryorchestration/internal/serviceconfig/database"
"github.com/golang-migrate/migrate/v4"
@@ -17,38 +16,52 @@ import (
_ "github.com/lib/pq"
)
func createDB(cfg database.ConfigProvider) error {
connStr := fmt.Sprintf("%s?%s", cfg.GetDBBaseURI(), cfg.GetDBOptsString())
func createDB(ctx context.Context, cfg database.ConfigProvider) error {
slog.Debug("creating connection to admin database", "uri", cfg.GetDBAdminDBURI())
db, err := sql.Open(cfg.GetDBDriver(), connStr)
db, err := sql.Open(cfg.GetDBDriver(), cfg.GetDBAdminDBURI())
if err != nil {
return fmt.Errorf("error opening admin database: %v", err)
}
slog.Debug("pinging admin database", "uri", cfg.GetDBAdminDBURI())
err = db.PingContext(ctx)
if err != nil {
return fmt.Errorf("error pinging admin database: %v", err)
}
slog.Debug("creating admin database", "uri", cfg.GetDBAdminDBURI())
_, err = db.Exec(fmt.Sprintf("CREATE DATABASE %s", cfg.GetDBName()))
if err != nil {
slog.Info("database not created", "name", cfg.GetDBName(), "error", err.Error())
} else {
slog.Info("database created", "name", cfg.GetDBName())
}
slog.Debug("creating connection to database", "uri", cfg.GetDBAdminDBURI())
db, err = sql.Open(cfg.GetDBDriver(), cfg.GetDBURI())
if err != nil {
return fmt.Errorf("error opening database: %v", err)
}
err = db.Ping()
slog.Debug("pinging database", "uri", cfg.GetDBAdminDBURI())
err = db.PingContext(ctx)
if err != nil {
return fmt.Errorf("error pinging database: %v", err)
}
rs, err := db.Query(fmt.Sprintf("SELECT 'CREATE DATABASE %s' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '%s')", cfg.GetDBName(), cfg.GetDBName()))
if err != nil {
return fmt.Errorf("error creating database: %v", err)
}
if rs.Next() {
slog.Info("database created", "name", cfg.GetDBName())
} else {
slog.Info("database already exists", "name", cfg.GetDBName())
}
return nil
}
//go:embed migrations/*.up.sql
var migrations embed.FS
func RunMigrations(ctx context.Context, cfg serviceconfig.ConfigProvider) error {
err := createDB(cfg)
func RunMigrations(ctx context.Context, cfg database.ConfigProvider) error {
err := createDB(ctx, cfg)
if err != nil {
return err
}
@@ -19,7 +19,6 @@ CREATE TYPE cleanFailType AS ENUM (
'invalid_read',
'invalid_read_pages',
'zero_page_count',
'large_page_count',
'large_file',
'small_dimensions',
'large_dimensions',
@@ -50,30 +49,20 @@ CREATE TABLE documentCleanEntries (
foreign key (cleanId) references documentCleans(id)
);
CREATE TABLE documentTextTriggerExtractions (
id uuid primary key DEFAULT uuid_generate_v7(),
cleanId uuid not null,
version bigint not null,
createdAt timestamp not null,
part unsignedsmallint not null,
textractJobId text,
foreign key (cleanId) references documentCleans(id)
);
CREATE TABLE documentTextExtractions (
id uuid primary key DEFAULT uuid_generate_v7(),
cleanId uuid not null,
bucket text not null,
key text not null,
hash text not null,
createdAt timestamp not null,
part unsignedsmallint not null
part unsignedsmallint not null,
foreign key (cleanId) references documentCleans(id)
);
CREATE TABLE documentTextExtractionEntries (
id uuid primary key DEFAULT uuid_generate_v7(),
textId uuid not null,
triggerId uuid not null,
version bigint not null,
foreign key (triggerId) references documentTextTriggerExtractions(id),
foreign key (textId) references documentTextExtractions(id)
);
@@ -54,25 +54,20 @@ WHERE row_num = 1;
CREATE VIEW currentTextEntries as
WITH RankedExtractions AS (
SELECT
dte.id,
extract.id,
cc.documentId,
dte.bucket,
dte.key,
dte.hash,
trigger.id as triggerId,
trigger.cleanId,
trigger.textractJobId,
trigger.version as triggerVersion,
extract.bucket,
extract.key,
extract.hash,
extract.cleanId,
dtee.version as extractionVersion,
ROW_NUMBER() OVER (PARTITION BY cc.documentId ORDER BY dte.id DESC) as row_num
ROW_NUMBER() OVER (PARTITION BY cc.documentId ORDER BY dtee.id DESC) as row_num
FROM documents d
JOIN currentCleanEntries cc on cc.documentId = d.id
JOIN currentCollectorMinTextVersions ctv ON ctv.clientId = d.clientId
JOIN documentTextTriggerExtractions trigger ON trigger.cleanId = cc.id
AND trigger.version >= ctv.minTextVersion
JOIN documentTextExtractionEntries dtee on dtee.triggerId = trigger.id
JOIN documentTextExtractions extract ON extract.cleanId = cc.id
JOIN documentTextExtractionEntries dtee on dtee.textId = extract.id
AND dtee.version >= ctv.minTextVersion
JOIN documentTextExtractions dte ON dte.id = dtee.textId
)
SELECT
id,
@@ -81,9 +76,6 @@ SELECT
key,
hash,
cleanId,
triggerId,
textractJobId,
triggerVersion,
extractionVersion
FROM RankedExtractions
WHERE row_num = 1;
+4 -10
View File
@@ -1,7 +1,6 @@
package database_test
import (
"context"
"testing"
migrations "queryorchestration/internal/database"
@@ -16,23 +15,18 @@ func TestRunMigrations(t *testing.T) {
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
ctx := t.Context()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{})
defer cleanup()
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{})
err := migrations.RunMigrations(ctx, cfg)
require.NoError(t, err)
}
func TestRunMigrationsNoDB(t *testing.T) {
ctx := context.Background()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
cfg.SetDBUser("invalid_user")
cfg.SetDBSecret("invalid_pass")
cfg.SetDBHost("invalid_host")
@@ -40,6 +34,6 @@ func TestRunMigrationsNoDB(t *testing.T) {
cfg.SetDBName("invalid_name")
cfg.SetDBNoSSL(true)
err := migrations.RunMigrations(ctx, cfg)
err := migrations.RunMigrations(t.Context(), cfg)
assert.Error(t, err)
}
+3 -1
View File
@@ -1,6 +1,7 @@
package database
import (
"context"
"testing"
"queryorchestration/internal/serviceconfig/database"
@@ -9,10 +10,11 @@ import (
)
func TestCreateDB(t *testing.T) {
ctx := context.Background()
cfg := &database.DBConfig{
DBHost: "invalid_value",
}
err := createDB(cfg)
err := createDB(ctx, cfg)
assert.Error(t, err)
}
+12 -35
View File
@@ -6,24 +6,18 @@ SELECT EXISTS(
-- name: GetDocumentTextExtractionByHash :one
SELECT id FROM currentTextEntries WHERE cleanId = @cleanEntryId and hash = @hash;
-- name: AddDocumentTextTrigger :one
INSERT INTO documentTextTriggerExtractions
(cleanId, version, createdAt, part) VALUES
($1, $2, $3, $4)
returning id;
-- name: GetTextractOutputCurrentPart :one
WITH client as (
select id from clients where id = @clientId
),
parts as (
SELECT triggers.part
SELECT extract.part
FROM client as c
JOIN documents as docs on docs.clientId = c.id
JOIN documentCleans as clean on docs.id = clean.documentId
JOIN documentTextTriggerExtractions as triggers
on triggers.cleanId = clean.id
WHERE date(triggers.createdAt) = date(@queryDate)
JOIN documentTextExtractions as extract
on extract.cleanId = clean.id
WHERE date(extract.createdAt) = date(@queryDate)
),
max_part as (
SELECT COALESCE(max(part), 0) as max_part_num FROM parts
@@ -40,17 +34,12 @@ WITH client as (
select id from clients where id = @clientId
),
parts as (
SELECT extractions.part
SELECT extract.part
FROM client as c
JOIN documents as docs on docs.clientId = c.id
JOIN documentCleans as clean on docs.id = clean.documentId
JOIN documentTextTriggerExtractions as triggers
on triggers.cleanId = clean.id
JOIN documentTextExtractionEntries as textEntries
on textEntries.triggerId = triggers.id
JOIN documentTextExtractions as extractions
on extractions.id = textEntries.textId
WHERE date(extractions.createdAt) = date(@queryDate)
JOIN documentTextExtractions extract ON extract.cleanId = clean.id
WHERE date(extract.createdAt) = date(@queryDate)
),
max_part as (
SELECT COALESCE(max(part), 0) as max_part_num FROM parts
@@ -62,30 +51,18 @@ FROM parts p
RIGHT JOIN max_part mp ON p.part = mp.max_part_num
GROUP BY p.part;
-- name: AddDocumentTextTriggerJobId :exec
UPDATE documentTextTriggerExtractions
SET textractJobId = @textractJobId
WHERE id = @id;
-- name: GetDocumentTextTrigger :one
SELECT dt.id, dt.cleanId, dt.version, dt.textractJobId, dc.documentId
from documentTextTriggerExtractions as dt
join documentCleans as dc on dt.cleanId = dc.id
where dt.id = @triggerId;
-- name: AddDocumentText :one
INSERT INTO documentTextExtractions
(bucket, key, hash, createdAt, part)
VALUES ($1, $2, $3, $4, $5)
(cleanId, bucket, key, hash, createdAt, part)
VALUES ($1, $2, $3, $4, $5, $6)
returning id;
-- name: AddDocumentTextEntry :exec
INSERT INTO documentTextExtractionEntries
(textId, triggerId, version)
VALUES ($1, $2, $3);
(textId, version)
VALUES ($1, $2);
-- name: GetTextEntryByDocId :one
SELECT id, documentId, bucket, key, hash, cleanId,
triggerId, textractJobId, triggerVersion, extractionVersion
SELECT id, documentId, bucket, key, hash, cleanId, extractionVersion
FROM currentTextEntries
WHERE documentId = @documentId;
+3 -3
View File
@@ -13,17 +13,17 @@ import (
)
func TestClean(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{
RunMigrations: true,
})
defer cleanup()
queries := cfg.GetDBQueries()
+3 -3
View File
@@ -13,17 +13,17 @@ import (
)
func TestClient(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{
RunMigrations: true,
})
defer cleanup()
queries := cfg.GetDBQueries()
@@ -15,17 +15,17 @@ import (
)
func TestCollector(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{
RunMigrations: true,
})
defer cleanup()
queries := cfg.GetDBQueries()
@@ -13,17 +13,17 @@ import (
)
func TestDocument(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{
RunMigrations: true,
})
defer cleanup()
queries := cfg.GetDBQueries()
+4 -18
View File
@@ -19,7 +19,6 @@ const (
CleanfailtypeInvalidRead Cleanfailtype = "invalid_read"
CleanfailtypeInvalidReadPages Cleanfailtype = "invalid_read_pages"
CleanfailtypeZeroPageCount Cleanfailtype = "zero_page_count"
CleanfailtypeLargePageCount Cleanfailtype = "large_page_count"
CleanfailtypeLargeFile Cleanfailtype = "large_file"
CleanfailtypeSmallDimensions Cleanfailtype = "small_dimensions"
CleanfailtypeLargeDimensions Cleanfailtype = "large_dimensions"
@@ -68,7 +67,6 @@ func (e Cleanfailtype) Valid() bool {
CleanfailtypeInvalidRead,
CleanfailtypeInvalidReadPages,
CleanfailtypeZeroPageCount,
CleanfailtypeLargePageCount,
CleanfailtypeLargeFile,
CleanfailtypeSmallDimensions,
CleanfailtypeLargeDimensions,
@@ -290,9 +288,6 @@ type Currenttextentry struct {
Key string `db:"key"`
Hash string `db:"hash"`
Cleanid uuid.UUID `db:"cleanid"`
Triggerid uuid.UUID `db:"triggerid"`
Textractjobid *string `db:"textractjobid"`
Triggerversion int64 `db:"triggerversion"`
Extractionversion int64 `db:"extractionversion"`
}
@@ -327,6 +322,7 @@ type Documententry struct {
type Documenttextextraction struct {
ID uuid.UUID `db:"id"`
Cleanid uuid.UUID `db:"cleanid"`
Bucket string `db:"bucket"`
Key string `db:"key"`
Hash string `db:"hash"`
@@ -335,19 +331,9 @@ type Documenttextextraction struct {
}
type Documenttextextractionentry struct {
ID uuid.UUID `db:"id"`
Textid uuid.UUID `db:"textid"`
Triggerid uuid.UUID `db:"triggerid"`
Version int64 `db:"version"`
}
type Documenttexttriggerextraction struct {
ID uuid.UUID `db:"id"`
Cleanid uuid.UUID `db:"cleanid"`
Version int64 `db:"version"`
Createdat pgtype.Timestamp `db:"createdat"`
Part uint16 `db:"part"`
Textractjobid *string `db:"textractjobid"`
ID uuid.UUID `db:"id"`
Textid uuid.UUID `db:"textid"`
Version int64 `db:"version"`
}
type Fullactivecollector struct {
+12 -12
View File
@@ -14,17 +14,17 @@ import (
)
func TestQueries(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{
RunMigrations: true,
})
defer cleanup()
queries := cfg.GetDBQueries()
@@ -226,17 +226,17 @@ func TestQueries(t *testing.T) {
}
func TestQueryDependencyTree(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{
RunMigrations: true,
})
defer cleanup()
queries := cfg.GetDBQueries()
@@ -447,17 +447,17 @@ func TestQueryDependencyTree(t *testing.T) {
}
func TestQueriesList(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{
RunMigrations: true,
})
defer cleanup()
queries := cfg.GetDBQueries()
@@ -505,17 +505,17 @@ func TestQueriesList(t *testing.T) {
}
func TestListQueryClients(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{
RunMigrations: true,
})
defer cleanup()
queries := cfg.GetDBQueries()
+31 -71
View File
@@ -16,17 +16,17 @@ import (
)
func TestResults(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{
RunMigrations: true,
})
defer cleanup()
queries := cfg.GetDBQueries()
@@ -97,9 +97,11 @@ func TestResults(t *testing.T) {
require.NoError(t, err)
assert.False(t, issynced)
triggerId, err := queries.AddDocumentTextTrigger(ctx, &repository.AddDocumentTextTriggerParams{
textId, err := queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Cleanid: cleanid,
Version: 123,
Bucket: "hi",
Key: "hello",
Hash: "example",
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
@@ -107,21 +109,9 @@ func TestResults(t *testing.T) {
},
})
require.NoError(t, err)
textId, err := queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Bucket: "hi",
Key: "hello",
Hash: "example",
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
Valid: true,
},
})
require.NoError(t, err)
err = queries.AddDocumentTextEntry(ctx, &repository.AddDocumentTextEntryParams{
Version: 1,
Textid: textId,
Triggerid: triggerId,
Version: 1,
Textid: textId,
})
require.NoError(t, err)
@@ -167,17 +157,17 @@ func TestResults(t *testing.T) {
}
func TestResultValues(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{
RunMigrations: true,
})
defer cleanup()
queries := cfg.GetDBQueries()
@@ -274,9 +264,11 @@ func TestResultValues(t *testing.T) {
Version: 1,
})
require.NoError(t, err)
triggerId, err := queries.AddDocumentTextTrigger(ctx, &repository.AddDocumentTextTriggerParams{
textId, err := queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Cleanid: cleanid,
Version: 123,
Bucket: "hi",
Key: "hello",
Hash: "example",
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
@@ -284,21 +276,9 @@ func TestResultValues(t *testing.T) {
},
})
require.NoError(t, err)
textId, err := queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Bucket: "hi",
Key: "hello",
Hash: "example",
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
Valid: true,
},
})
require.NoError(t, err)
err = queries.AddDocumentTextEntry(ctx, &repository.AddDocumentTextEntryParams{
Version: 1,
Textid: textId,
Triggerid: triggerId,
Version: 1,
Textid: textId,
})
require.NoError(t, err)
@@ -393,17 +373,17 @@ func TestResultValues(t *testing.T) {
}
func TestUnsyncedNoDepsQueries(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{
RunMigrations: true,
})
defer cleanup()
queries := cfg.GetDBQueries()
@@ -498,9 +478,10 @@ func TestUnsyncedNoDepsQueries(t *testing.T) {
require.NoError(t, err)
assert.Len(t, qs, 0)
triggerId, err := queries.AddDocumentTextTrigger(ctx, &repository.AddDocumentTextTriggerParams{
textId, err := queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Cleanid: cleanid,
Version: 123,
Bucket: "hi",
Key: "hello",
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
@@ -508,20 +489,9 @@ func TestUnsyncedNoDepsQueries(t *testing.T) {
},
})
require.NoError(t, err)
textId, err := queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Bucket: "hi",
Key: "hello",
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
Valid: true,
},
})
require.NoError(t, err)
err = queries.AddDocumentTextEntry(ctx, &repository.AddDocumentTextEntryParams{
Version: 1,
Textid: textId,
Triggerid: triggerId,
Version: 1,
Textid: textId,
})
require.NoError(t, err)
@@ -588,9 +558,10 @@ func TestUnsyncedNoDepsQueries(t *testing.T) {
require.NoError(t, err)
assert.Len(t, qs, 0)
triggerTwoId, err := queries.AddDocumentTextTrigger(ctx, &repository.AddDocumentTextTriggerParams{
textTwoId, err := queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Cleanid: cleantwoid,
Version: 123,
Bucket: "hi",
Key: "hello",
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
@@ -598,20 +569,9 @@ func TestUnsyncedNoDepsQueries(t *testing.T) {
},
})
require.NoError(t, err)
textTwoId, err := queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Bucket: "hi",
Key: "hello",
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
Valid: true,
},
})
require.NoError(t, err)
err = queries.AddDocumentTextEntry(ctx, &repository.AddDocumentTextEntryParams{
Version: 1,
Textid: textTwoId,
Triggerid: triggerTwoId,
Version: 1,
Textid: textTwoId,
})
require.NoError(t, err)
+15 -45
View File
@@ -15,17 +15,17 @@ import (
)
func TestListClientDocumentIDs(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{
RunMigrations: true,
})
defer cleanup()
queries := cfg.GetDBQueries()
@@ -120,17 +120,17 @@ func TestListClientDocumentIDs(t *testing.T) {
}
func TestClientSync(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{
RunMigrations: true,
})
defer cleanup()
queries := cfg.GetDBQueries()
@@ -285,31 +285,21 @@ func TestClientSync(t *testing.T) {
Fields: []byte(`{"first_key": null}`),
}, docExternal)
triggerId, err := queries.AddDocumentTextTrigger(ctx, &repository.AddDocumentTextTriggerParams{
textId, err := queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Cleanid: cleanId,
Version: 123,
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
Valid: true,
},
})
require.NoError(t, err)
textId, err := queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Bucket: "hi",
Key: "hello",
Hash: "example",
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
Valid: true,
},
})
require.NoError(t, err)
err = queries.AddDocumentTextEntry(ctx, &repository.AddDocumentTextEntryParams{
Version: 1,
Textid: textId,
Triggerid: triggerId,
Version: 1,
Textid: textId,
})
require.NoError(t, err)
@@ -467,16 +457,6 @@ func TestClientSync(t *testing.T) {
})
t.Run("update text entry", func(t *testing.T) {
triggerId, err := queries.AddDocumentTextTrigger(ctx, &repository.AddDocumentTextTriggerParams{
Cleanid: cleanId,
Version: 123,
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
Valid: true,
},
})
require.NoError(t, err)
textId, err := queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Bucket: "hi",
Key: "hello",
@@ -485,12 +465,12 @@ func TestClientSync(t *testing.T) {
Time: time.Now().UTC(),
Valid: true,
},
Cleanid: cleanId,
})
require.NoError(t, err)
err = queries.AddDocumentTextEntry(ctx, &repository.AddDocumentTextEntryParams{
Version: 1,
Textid: textId,
Triggerid: triggerId,
Version: 1,
Textid: textId,
})
require.NoError(t, err)
@@ -594,16 +574,6 @@ func TestClientSync(t *testing.T) {
Fields: []byte(`{"first_key": null}`),
}, docExternal)
triggerThreeId, err := queries.AddDocumentTextTrigger(ctx, &repository.AddDocumentTextTriggerParams{
Cleanid: cleanthreeid,
Version: 123,
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
Valid: true,
},
})
require.NoError(t, err)
textThreeId, err := queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Bucket: "hi",
Key: "hello",
@@ -612,12 +582,12 @@ func TestClientSync(t *testing.T) {
Time: time.Now().UTC(),
Valid: true,
},
Cleanid: cleanthreeid,
})
require.NoError(t, err)
err = queries.AddDocumentTextEntry(ctx, &repository.AddDocumentTextEntryParams{
Version: 1,
Textid: textThreeId,
Triggerid: triggerThreeId,
Version: 1,
Textid: textThreeId,
})
require.NoError(t, err)
+29 -130
View File
@@ -14,12 +14,13 @@ import (
const addDocumentText = `-- name: AddDocumentText :one
INSERT INTO documentTextExtractions
(bucket, key, hash, createdAt, part)
VALUES ($1, $2, $3, $4, $5)
(cleanId, bucket, key, hash, createdAt, part)
VALUES ($1, $2, $3, $4, $5, $6)
returning id
`
type AddDocumentTextParams struct {
Cleanid uuid.UUID `db:"cleanid"`
Bucket string `db:"bucket"`
Key string `db:"key"`
Hash string `db:"hash"`
@@ -30,11 +31,12 @@ type AddDocumentTextParams struct {
// AddDocumentText
//
// INSERT INTO documentTextExtractions
// (bucket, key, hash, createdAt, part)
// VALUES ($1, $2, $3, $4, $5)
// (cleanId, bucket, key, hash, createdAt, part)
// VALUES ($1, $2, $3, $4, $5, $6)
// returning id
func (q *Queries) AddDocumentText(ctx context.Context, arg *AddDocumentTextParams) (uuid.UUID, error) {
row := q.db.QueryRow(ctx, addDocumentText,
arg.Cleanid,
arg.Bucket,
arg.Key,
arg.Hash,
@@ -48,76 +50,22 @@ func (q *Queries) AddDocumentText(ctx context.Context, arg *AddDocumentTextParam
const addDocumentTextEntry = `-- name: AddDocumentTextEntry :exec
INSERT INTO documentTextExtractionEntries
(textId, triggerId, version)
VALUES ($1, $2, $3)
(textId, version)
VALUES ($1, $2)
`
type AddDocumentTextEntryParams struct {
Textid uuid.UUID `db:"textid"`
Triggerid uuid.UUID `db:"triggerid"`
Version int64 `db:"version"`
Textid uuid.UUID `db:"textid"`
Version int64 `db:"version"`
}
// AddDocumentTextEntry
//
// INSERT INTO documentTextExtractionEntries
// (textId, triggerId, version)
// VALUES ($1, $2, $3)
// (textId, version)
// VALUES ($1, $2)
func (q *Queries) AddDocumentTextEntry(ctx context.Context, arg *AddDocumentTextEntryParams) error {
_, err := q.db.Exec(ctx, addDocumentTextEntry, arg.Textid, arg.Triggerid, arg.Version)
return err
}
const addDocumentTextTrigger = `-- name: AddDocumentTextTrigger :one
INSERT INTO documentTextTriggerExtractions
(cleanId, version, createdAt, part) VALUES
($1, $2, $3, $4)
returning id
`
type AddDocumentTextTriggerParams struct {
Cleanid uuid.UUID `db:"cleanid"`
Version int64 `db:"version"`
Createdat pgtype.Timestamp `db:"createdat"`
Part uint16 `db:"part"`
}
// AddDocumentTextTrigger
//
// INSERT INTO documentTextTriggerExtractions
// (cleanId, version, createdAt, part) VALUES
// ($1, $2, $3, $4)
// returning id
func (q *Queries) AddDocumentTextTrigger(ctx context.Context, arg *AddDocumentTextTriggerParams) (uuid.UUID, error) {
row := q.db.QueryRow(ctx, addDocumentTextTrigger,
arg.Cleanid,
arg.Version,
arg.Createdat,
arg.Part,
)
var id uuid.UUID
err := row.Scan(&id)
return id, err
}
const addDocumentTextTriggerJobId = `-- name: AddDocumentTextTriggerJobId :exec
UPDATE documentTextTriggerExtractions
SET textractJobId = $1
WHERE id = $2
`
type AddDocumentTextTriggerJobIdParams struct {
Textractjobid *string `db:"textractjobid"`
ID uuid.UUID `db:"id"`
}
// AddDocumentTextTriggerJobId
//
// UPDATE documentTextTriggerExtractions
// SET textractJobId = $1
// WHERE id = $2
func (q *Queries) AddDocumentTextTriggerJobId(ctx context.Context, arg *AddDocumentTextTriggerJobIdParams) error {
_, err := q.db.Exec(ctx, addDocumentTextTriggerJobId, arg.Textractjobid, arg.ID)
_, err := q.db.Exec(ctx, addDocumentTextEntry, arg.Textid, arg.Version)
return err
}
@@ -140,51 +88,15 @@ func (q *Queries) GetDocumentTextExtractionByHash(ctx context.Context, arg *GetD
return id, err
}
const getDocumentTextTrigger = `-- name: GetDocumentTextTrigger :one
SELECT dt.id, dt.cleanId, dt.version, dt.textractJobId, dc.documentId
from documentTextTriggerExtractions as dt
join documentCleans as dc on dt.cleanId = dc.id
where dt.id = $1
`
type GetDocumentTextTriggerRow struct {
ID uuid.UUID `db:"id"`
Cleanid uuid.UUID `db:"cleanid"`
Version int64 `db:"version"`
Textractjobid *string `db:"textractjobid"`
Documentid uuid.UUID `db:"documentid"`
}
// GetDocumentTextTrigger
//
// SELECT dt.id, dt.cleanId, dt.version, dt.textractJobId, dc.documentId
// from documentTextTriggerExtractions as dt
// join documentCleans as dc on dt.cleanId = dc.id
// where dt.id = $1
func (q *Queries) GetDocumentTextTrigger(ctx context.Context, triggerid uuid.UUID) (*GetDocumentTextTriggerRow, error) {
row := q.db.QueryRow(ctx, getDocumentTextTrigger, triggerid)
var i GetDocumentTextTriggerRow
err := row.Scan(
&i.ID,
&i.Cleanid,
&i.Version,
&i.Textractjobid,
&i.Documentid,
)
return &i, err
}
const getTextEntryByDocId = `-- name: GetTextEntryByDocId :one
SELECT id, documentId, bucket, key, hash, cleanId,
triggerId, textractJobId, triggerVersion, extractionVersion
SELECT id, documentId, bucket, key, hash, cleanId, extractionVersion
FROM currentTextEntries
WHERE documentId = $1
`
// GetTextEntryByDocId
//
// SELECT id, documentId, bucket, key, hash, cleanId,
// triggerId, textractJobId, triggerVersion, extractionVersion
// SELECT id, documentId, bucket, key, hash, cleanId, extractionVersion
// FROM currentTextEntries
// WHERE documentId = $1
func (q *Queries) GetTextEntryByDocId(ctx context.Context, documentid uuid.UUID) (*Currenttextentry, error) {
@@ -197,9 +109,6 @@ func (q *Queries) GetTextEntryByDocId(ctx context.Context, documentid uuid.UUID)
&i.Key,
&i.Hash,
&i.Cleanid,
&i.Triggerid,
&i.Textractjobid,
&i.Triggerversion,
&i.Extractionversion,
)
return &i, err
@@ -210,17 +119,12 @@ WITH client as (
select id from clients where id = $1
),
parts as (
SELECT extractions.part
SELECT extract.part
FROM client as c
JOIN documents as docs on docs.clientId = c.id
JOIN documentCleans as clean on docs.id = clean.documentId
JOIN documentTextTriggerExtractions as triggers
on triggers.cleanId = clean.id
JOIN documentTextExtractionEntries as textEntries
on textEntries.triggerId = triggers.id
JOIN documentTextExtractions as extractions
on extractions.id = textEntries.textId
WHERE date(extractions.createdAt) = date($2)
JOIN documentTextExtractions extract ON extract.cleanId = clean.id
WHERE date(extract.createdAt) = date($2)
),
max_part as (
SELECT COALESCE(max(part), 0) as max_part_num FROM parts
@@ -249,17 +153,12 @@ type GetTextOutCurrentPartRow struct {
// select id from clients where id = $1
// ),
// parts as (
// SELECT extractions.part
// SELECT extract.part
// FROM client as c
// JOIN documents as docs on docs.clientId = c.id
// JOIN documentCleans as clean on docs.id = clean.documentId
// JOIN documentTextTriggerExtractions as triggers
// on triggers.cleanId = clean.id
// JOIN documentTextExtractionEntries as textEntries
// on textEntries.triggerId = triggers.id
// JOIN documentTextExtractions as extractions
// on extractions.id = textEntries.textId
// WHERE date(extractions.createdAt) = date($2)
// JOIN documentTextExtractions extract ON extract.cleanId = clean.id
// WHERE date(extract.createdAt) = date($2)
// ),
// max_part as (
// SELECT COALESCE(max(part), 0) as max_part_num FROM parts
@@ -282,13 +181,13 @@ WITH client as (
select id from clients where id = $1
),
parts as (
SELECT triggers.part
SELECT extract.part
FROM client as c
JOIN documents as docs on docs.clientId = c.id
JOIN documentCleans as clean on docs.id = clean.documentId
JOIN documentTextTriggerExtractions as triggers
on triggers.cleanId = clean.id
WHERE date(triggers.createdAt) = date($2)
JOIN documentTextExtractions as extract
on extract.cleanId = clean.id
WHERE date(extract.createdAt) = date($2)
),
max_part as (
SELECT COALESCE(max(part), 0) as max_part_num FROM parts
@@ -317,13 +216,13 @@ type GetTextractOutputCurrentPartRow struct {
// select id from clients where id = $1
// ),
// parts as (
// SELECT triggers.part
// SELECT extract.part
// FROM client as c
// JOIN documents as docs on docs.clientId = c.id
// JOIN documentCleans as clean on docs.id = clean.documentId
// JOIN documentTextTriggerExtractions as triggers
// on triggers.cleanId = clean.id
// WHERE date(triggers.createdAt) = date($2)
// JOIN documentTextExtractions as extract
// on extract.cleanId = clean.id
// WHERE date(extract.createdAt) = date($2)
// ),
// max_part as (
// SELECT COALESCE(max(part), 0) as max_part_num FROM parts
+53 -125
View File
@@ -16,17 +16,17 @@ import (
)
func TestTextExtraction(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, textup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{
RunMigrations: true,
})
defer textup()
queries := cfg.GetDBQueries()
@@ -68,73 +68,31 @@ func TestTextExtraction(t *testing.T) {
require.NoError(t, err)
assert.False(t, isextract)
triggerId, err := queries.AddDocumentTextTrigger(ctx, &repository.AddDocumentTextTriggerParams{
Cleanid: cleanid,
Version: 123,
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
Valid: true,
},
})
require.NoError(t, err)
trigger, err := queries.GetDocumentTextTrigger(ctx, triggerId)
require.NoError(t, err)
assert.EqualExportedValues(t, &repository.GetDocumentTextTriggerRow{
ID: triggerId,
Cleanid: cleanid,
Version: 123,
Documentid: id,
}, trigger)
isextract, err = queries.IsDocumentTextExtracted(ctx, id)
require.NoError(t, err)
assert.False(t, isextract)
textractId := "hello"
err = queries.AddDocumentTextTriggerJobId(ctx, &repository.AddDocumentTextTriggerJobIdParams{
Textractjobid: &textractId,
ID: triggerId,
})
require.NoError(t, err)
trigger, err = queries.GetDocumentTextTrigger(ctx, triggerId)
require.NoError(t, err)
assert.EqualExportedValues(t, &repository.GetDocumentTextTriggerRow{
ID: triggerId,
Cleanid: cleanid,
Version: 123,
Textractjobid: &textractId,
Documentid: id,
}, trigger)
isextract, err = queries.IsDocumentTextExtracted(ctx, id)
require.NoError(t, err)
assert.False(t, isextract)
textId, err := queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Bucket: bucket,
Key: key,
Hash: "example",
Part: 0,
Cleanid: cleanid,
Part: 0,
Bucket: bucket,
Key: key,
Hash: "example",
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
Valid: true,
},
})
require.NoError(t, err)
assert.NotNil(t, textId)
assert.NotEqual(t, uuid.UUID{}, textId)
isextract, err = queries.IsDocumentTextExtracted(ctx, id)
require.NoError(t, err)
assert.False(t, isextract)
isextract, err = queries.IsDocumentTextExtracted(ctx, id)
require.NoError(t, err)
assert.False(t, isextract)
err = queries.AddDocumentTextEntry(ctx, &repository.AddDocumentTextEntryParams{
Textid: textId,
Triggerid: triggerId,
Version: 543,
Textid: textId,
Version: 543,
})
require.NoError(t, err)
@@ -150,9 +108,6 @@ func TestTextExtraction(t *testing.T) {
assert.Equal(t, key, text.Key)
assert.Equal(t, "example", text.Hash)
assert.Equal(t, cleanid, text.Cleanid)
assert.Equal(t, triggerId, text.Triggerid)
assert.Equal(t, textractId, *text.Textractjobid)
assert.Equal(t, int64(123), text.Triggerversion)
assert.Equal(t, int64(543), text.Extractionversion)
assert.Equal(t, textId, text.ID)
@@ -171,17 +126,17 @@ func TestTextExtraction(t *testing.T) {
}
func TestTextTextractPart(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, textup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{
RunMigrations: true,
})
defer textup()
queries := cfg.GetDBQueries()
@@ -232,9 +187,8 @@ func TestTextTextractPart(t *testing.T) {
Part: 0,
}, part)
_, err = queries.AddDocumentTextTrigger(ctx, &repository.AddDocumentTextTriggerParams{
_, err = queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Cleanid: cleanid,
Version: 123,
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
@@ -256,9 +210,8 @@ func TestTextTextractPart(t *testing.T) {
Part: 0,
}, part)
_, err = queries.AddDocumentTextTrigger(ctx, &repository.AddDocumentTextTriggerParams{
_, err = queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Cleanid: cleanid,
Version: 123,
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
@@ -280,9 +233,8 @@ func TestTextTextractPart(t *testing.T) {
Part: 0,
}, part)
_, err = queries.AddDocumentTextTrigger(ctx, &repository.AddDocumentTextTriggerParams{
_, err = queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Cleanid: cleanid,
Version: 123,
Part: 1,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
@@ -306,17 +258,17 @@ func TestTextTextractPart(t *testing.T) {
}
func TestTextOutPart(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, textup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{
RunMigrations: true,
})
defer textup()
queries := cfg.GetDBQueries()
@@ -354,9 +306,11 @@ func TestTextOutPart(t *testing.T) {
})
require.NoError(t, err)
triggerId, err := queries.AddDocumentTextTrigger(ctx, &repository.AddDocumentTextTriggerParams{
textId, err := queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Cleanid: cleanid,
Version: 123,
Bucket: "hi",
Key: "hi",
Hash: "hi",
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
@@ -364,6 +318,11 @@ func TestTextOutPart(t *testing.T) {
},
})
require.NoError(t, err)
err = queries.AddDocumentTextEntry(ctx, &repository.AddDocumentTextEntryParams{
Textid: textId,
Version: 123,
})
require.NoError(t, err)
part, err := queries.GetTextOutCurrentPart(ctx, &repository.GetTextOutCurrentPartParams{
Clientid: &clientId,
@@ -373,47 +332,17 @@ func TestTextOutPart(t *testing.T) {
},
})
require.NoError(t, err)
assert.Equal(t, &repository.GetTextOutCurrentPartRow{
Count: 0,
Part: 0,
}, part)
textId, err := queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Bucket: "hi",
Key: "hi",
Hash: "hi",
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
Valid: true,
},
})
require.NoError(t, err)
err = queries.AddDocumentTextEntry(ctx, &repository.AddDocumentTextEntryParams{
Textid: textId,
Triggerid: triggerId,
Version: 123,
})
require.NoError(t, err)
part, err = queries.GetTextOutCurrentPart(ctx, &repository.GetTextOutCurrentPartParams{
Clientid: &clientId,
Querydate: pgtype.Timestamptz{
Time: time.Now().UTC(),
Valid: true,
},
})
require.NoError(t, err)
assert.Equal(t, &repository.GetTextOutCurrentPartRow{
Count: 1,
Part: 0,
}, part)
textId, err = queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Bucket: "hi",
Key: "hi",
Hash: "hi",
Part: 0,
Cleanid: cleanid,
Bucket: "hi",
Key: "hi",
Hash: "hi",
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
Valid: true,
@@ -421,9 +350,8 @@ func TestTextOutPart(t *testing.T) {
})
require.NoError(t, err)
err = queries.AddDocumentTextEntry(ctx, &repository.AddDocumentTextEntryParams{
Textid: textId,
Triggerid: triggerId,
Version: 123,
Textid: textId,
Version: 123,
})
require.NoError(t, err)
@@ -441,10 +369,11 @@ func TestTextOutPart(t *testing.T) {
}, part)
textId, err = queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Bucket: "hi",
Key: "hi",
Hash: "hi",
Part: 0,
Cleanid: cleanid,
Bucket: "hi",
Key: "hi",
Hash: "hi",
Part: 0,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
Valid: true,
@@ -452,9 +381,8 @@ func TestTextOutPart(t *testing.T) {
})
require.NoError(t, err)
err = queries.AddDocumentTextEntry(ctx, &repository.AddDocumentTextEntryParams{
Textid: textId,
Triggerid: triggerId,
Version: 123,
Textid: textId,
Version: 123,
})
require.NoError(t, err)
@@ -472,10 +400,11 @@ func TestTextOutPart(t *testing.T) {
}, part)
textId, err = queries.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Bucket: "hi",
Key: "hi",
Hash: "hi",
Part: 1,
Cleanid: cleanid,
Bucket: "hi",
Key: "hi",
Hash: "hi",
Part: 1,
Createdat: pgtype.Timestamp{
Time: time.Now().UTC(),
Valid: true,
@@ -483,9 +412,8 @@ func TestTextOutPart(t *testing.T) {
})
require.NoError(t, err)
err = queries.AddDocumentTextEntry(ctx, &repository.AddDocumentTextEntryParams{
Textid: textId,
Triggerid: triggerId,
Version: 123,
Textid: textId,
Version: 123,
})
require.NoError(t, err)
+6 -1
View File
@@ -57,7 +57,12 @@ func (s *Service) executeCleanTasks(ctx context.Context, params *CleanParams) (*
}, nil
}
content, err := s.getContent(ctx, params, mimeType)
content, err := documenttypes.GetFile(ctx, s.cfg, documenttypes.GetFileParams{
Bucket: params.Bucket,
Hash: params.Hash,
Key: params.Key,
Mimetype: mimeType,
})
if err != nil {
return nil, err
}
-41
View File
@@ -1,41 +0,0 @@
package documentclean
import (
"context"
"errors"
documenttypes "queryorchestration/internal/document/types"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
type Content interface {
IsCorrupt(context.Context) *documenttypes.InvalidDocumentReason
}
func (s *Service) getContent(ctx context.Context, params *CleanParams, mimeType documenttypes.MimeType) (Content, error) {
key := params.Key.String()
resp, err := s.cfg.GetStoreClient().GetObject(ctx, &s3.GetObjectInput{
Bucket: &params.Bucket,
Key: &key,
IfMatch: &params.Hash,
})
if err != nil {
return nil, err
}
defer resp.Body.Close()
var content Content
switch mimeType {
case documenttypes.MimeTypePDF:
pdf, err := documenttypes.NewPDFFromReadCloser(resp.Body)
if err != nil {
return nil, err
}
content = pdf
default:
return nil, errors.New("invalid mimetype")
}
return content, nil
}
-65
View File
@@ -1,65 +0,0 @@
package documentclean
import (
"context"
"io"
"strings"
"testing"
"time"
documenttypes "queryorchestration/internal/document/types"
"queryorchestration/internal/serviceconfig/objectstore"
objectstoremock "queryorchestration/mocks/objectstore"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestGetContent(t *testing.T) {
t.Run("Working", func(t *testing.T) {
ctx := context.Background()
cfg := &DocCleanConfig{}
mockS3 := objectstoremock.NewMockS3Client(t)
cfg.StoreClient = mockS3
svc := Service{
cfg: cfg,
}
bucket := "example_bucket"
key := objectstore.BucketKey{
ClientID: "ddddddd",
CreatedAt: time.Now().UTC(),
EntityID: uuid.New(),
Location: objectstore.Import,
}
params := &CleanParams{
Hash: "hash",
Bucket: bucket,
Key: key,
}
mimeType := documenttypes.MimeTypePDF
body := io.NopCloser(strings.NewReader(pdfHelloWorld))
mockS3.EXPECT().
GetObject(
mock.Anything,
mock.MatchedBy(func(in *s3.GetObjectInput) bool {
return *in.Bucket == bucket && *in.Key == key.String()
}),
mock.Anything,
).
Return(&s3.GetObjectOutput{
Body: body,
}, nil)
content, err := svc.getContent(ctx, params, mimeType)
require.NoError(t, err)
assert.NotNil(t, content)
})
}
-8
View File
@@ -5,7 +5,6 @@ import (
"log/slog"
docinitrunner "queryorchestration/api/docInitRunner"
doctextprocessrunner "queryorchestration/api/docTextProcessRunner"
"queryorchestration/internal/serviceconfig/objectstore"
"queryorchestration/internal/serviceconfig/queue"
)
@@ -44,13 +43,6 @@ func (s *Service) Process(ctx context.Context, params Params) error {
Key: params.Key,
Hash: params.Hash,
}
case objectstore.TextTextract:
queueParams.QueueURL = s.cfg.GetDocumentTextProcessURL()
queueParams.Body = doctextprocessrunner.Body{
Bucket: params.Bucket,
Key: params.Key,
Hash: params.Hash,
}
default:
slog.Info("unsupported key", "key", params.Key)
return nil
-25
View File
@@ -8,7 +8,6 @@ import (
"queryorchestration/internal/serviceconfig"
"queryorchestration/internal/serviceconfig/objectstore"
"queryorchestration/internal/serviceconfig/queue/documentinit"
"queryorchestration/internal/serviceconfig/queue/documenttextprocess"
queuemock "queryorchestration/mocks/queue"
"github.com/aws/aws-sdk-go-v2/service/sqs"
@@ -19,7 +18,6 @@ import (
type ProcessConfig struct {
serviceconfig.BaseConfig
documentinit.DocInitConfig
documenttextprocess.DocTextProcessConfig
}
func TestProcess(t *testing.T) {
@@ -29,7 +27,6 @@ func TestProcess(t *testing.T) {
mockSQS := queuemock.NewMockSQSClient(t)
cfg.QueueClient = mockSQS
cfg.DocInitURL = "docinit"
cfg.DocumentTextProcessURL = "doctextprocess"
svc := New(cfg)
@@ -68,28 +65,6 @@ func TestProcess(t *testing.T) {
})
assert.NoError(t, err)
})
t.Run("document text process", func(t *testing.T) {
mockSQS.EXPECT().
SendMessage(
mock.Anything,
mock.MatchedBy(func(in *sqs.SendMessageInput) bool {
return *in.QueueUrl == cfg.DocumentTextProcessURL
}),
mock.Anything,
).
Return(&sqs.SendMessageOutput{}, nil)
location := objectstore.BucketKey{
ClientID: "7db16095-9155-47d4-8004-b3b3ead93c83",
Location: objectstore.TextTextract,
CreatedAt: time.Now().UTC(),
}
err := svc.Process(ctx, Params{
Event: EventS3ObjectCreatedPut,
Key: location.String(),
})
assert.NoError(t, err)
})
}
func TestIsSupportedEvent(t *testing.T) {
-2
View File
@@ -3,13 +3,11 @@ package documentstore
import (
"queryorchestration/internal/serviceconfig"
"queryorchestration/internal/serviceconfig/queue/documentinit"
"queryorchestration/internal/serviceconfig/queue/documenttextprocess"
)
type ConfigProvider interface {
serviceconfig.ConfigProvider
documentinit.ConfigProvider
documenttextprocess.ConfigProvider
}
type Service struct {
-2
View File
@@ -5,7 +5,6 @@ import (
"queryorchestration/internal/serviceconfig"
"queryorchestration/internal/serviceconfig/queue/documentinit"
"queryorchestration/internal/serviceconfig/queue/documenttextprocess"
"github.com/stretchr/testify/assert"
)
@@ -13,7 +12,6 @@ import (
type DocInitConfig struct {
serviceconfig.BaseConfig
documentinit.DocInitConfig
documenttextprocess.DocTextProcessConfig
}
func TestNew(t *testing.T) {
+43
View File
@@ -0,0 +1,43 @@
package documenttext
import (
"context"
"fmt"
"io"
"strings"
documenttypes "queryorchestration/internal/document/types"
)
func (s *Service) getDocumentText(ctx context.Context, doc documenttypes.File) (io.Reader, error) {
pagesNum, err := doc.GetPageCount(ctx)
if err != nil {
return nil, err
}
textPages := make([]string, pagesNum)
for i := range pagesNum {
text, err := s.getPageText(ctx, doc, i)
if err != nil {
return nil, err
}
textPages[i] = text
}
text, err := s.mergeDocument(textPages)
if err != nil {
return nil, err
}
return text, nil
}
func (s *Service) mergeDocument(pages []string) (io.Reader, error) {
var builder strings.Builder
for i, page := range pages {
builder.WriteString(fmt.Sprintf("Start of Page No. = %d\n\n", i+1))
builder.WriteString(page)
builder.WriteString("\n\n")
}
return strings.NewReader(builder.String()), nil
}
+48
View File
@@ -0,0 +1,48 @@
package documenttext
import (
"context"
"log/slog"
documenttypes "queryorchestration/internal/document/types"
"queryorchestration/internal/serviceconfig/objectstore"
"github.com/google/uuid"
)
func (s *Service) executeExtraction(ctx context.Context, documentId uuid.UUID) error {
clean, err := s.cfg.GetDBQueries().GetCleanEntryByDocId(ctx, documentId)
if err != nil {
return err
} else if clean.Fail.Valid {
slog.Error("document has no clean document", "id", documentId)
return nil
}
key, err := objectstore.ParseBucketKey(*clean.Key)
if err != nil {
return err
}
doc, err := documenttypes.GetFile(ctx, s.cfg, documenttypes.GetFileParams{
Bucket: *clean.Bucket,
Key: key,
Hash: *clean.Hash,
Mimetype: documenttypes.ParseDBNullMimeType(clean.Mimetype),
})
if err != nil {
return err
}
text, err := s.getDocumentText(ctx, doc)
if err != nil {
return err
}
err = s.storeProcess(ctx, text, clean)
if err != nil {
return err
}
return nil
}
+102
View File
@@ -0,0 +1,102 @@
package documenttext
import (
"context"
"io"
"strings"
"testing"
"time"
"queryorchestration/internal/database/repository"
"queryorchestration/internal/serviceconfig/objectstore"
objectstoremock "queryorchestration/mocks/objectstore"
textractmock "queryorchestration/mocks/textract"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/textract"
"github.com/google/uuid"
"github.com/pashagolub/pgxmock/v3"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestExecuteExtract(t *testing.T) {
ctx := context.Background()
pool, err := pgxmock.NewPool()
require.NoError(t, err)
cfg := &DocTextConfig{}
cfg.DBPool = pool
cfg.DBQueries = repository.New(pool)
mockStore := objectstoremock.NewMockS3Client(t)
cfg.StoreClient = mockStore
mockTextract := textractmock.NewMockTextractClient(t)
cfg.TextractClient = mockTextract
svc := Service{
cfg: cfg,
}
cleanId := uuid.New()
docId := uuid.New()
bucket := "bucket"
hash := `"a06d5e0e795adeb6d0b3732330dbcc15"`
textId := uuid.New()
key := objectstore.BucketKey{
ClientID: "aa",
Location: objectstore.Import,
CreatedAt: time.Now().UTC(),
EntityID: cleanId,
}
keyStr := key.String()
textractFile := "../../../assets/sampleGeneration/generated/helloWorld.gen"
textractBaseOut := getTextractFileResponse(t, textractFile)
mimetype := repository.NullCleanmimetype{
Valid: true,
Cleanmimetype: repository.CleanmimetypeApplicationPdf,
}
pool.ExpectQuery("name: GetCleanEntryByDocId :one").WithArgs(docId).
WillReturnRows(
pgxmock.NewRows([]string{"id", "documentId", "bucket", "key", "version", "hash", "mimetype", "fail", "clientId"}).
AddRow(cleanId, docId, &bucket, &keyStr, int64(123), &hash, mimetype, nil, key.ClientID),
)
bodyMsg := io.NopCloser(strings.NewReader(pdfHelloWorld))
mockStore.EXPECT().
GetObject(
mock.Anything,
mock.MatchedBy(func(in *s3.GetObjectInput) bool {
return *in.Bucket == bucket && *in.Key == keyStr
}),
mock.Anything,
).
Return(&s3.GetObjectOutput{
Body: bodyMsg,
}, nil)
sent := 0
mockTextract.EXPECT().
AnalyzeDocument(
mock.Anything,
mock.MatchedBy(func(in *textract.AnalyzeDocumentInput) bool {
if sent == 0 {
sent++
return true
}
return false
}),
mock.Anything,
).
Return(&textractBaseOut["base"][0], nil).Once()
pool.ExpectBegin()
pool.ExpectQuery("name: GetDocumentTextExtractionByHash :one").WithArgs(cleanId, hash).WillReturnRows(
pgxmock.NewRows([]string{"id"}).
AddRow(textId),
)
pool.ExpectExec("name: AddDocumentTextEntry :exec").WithArgs(textId, pgxmock.AnyArg()).
WillReturnResult(pgxmock.NewResult("", 1))
pool.ExpectCommit()
err = svc.executeExtraction(ctx, docId)
require.NoError(t, err)
}
+6 -14
View File
@@ -16,24 +16,16 @@ func (s *Service) Extract(ctx context.Context, documentId uuid.UUID) error {
isextracted, err := s.cfg.GetDBQueries().IsDocumentTextExtracted(ctx, documentId)
if err != nil {
return err
} else if isextracted {
return s.informExtraction(ctx, documentId)
}
entry, err := s.cfg.GetDBQueries().GetCleanEntryByDocId(ctx, documentId)
if err != nil {
return err
} else if entry.Fail.Valid {
slog.Error("document has no clean document", "id", documentId)
return nil
if !isextracted {
err = s.executeExtraction(ctx, documentId)
if err != nil {
return err
}
}
err = s.triggerExtract(ctx, entry)
if err != nil {
return err
}
return nil
return s.informExtraction(ctx, documentId)
}
func (s *Service) informExtraction(ctx context.Context, id uuid.UUID) error {
+83 -55
View File
@@ -3,7 +3,10 @@ package documenttext
import (
"context"
"fmt"
"io"
"strings"
"testing"
"time"
"queryorchestration/internal/database/repository"
"queryorchestration/internal/serviceconfig"
@@ -15,13 +18,13 @@ import (
queuemock "queryorchestration/mocks/queue"
textractmock "queryorchestration/mocks/textract"
"github.com/stretchr/testify/require"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/sqs"
awstextract "github.com/aws/aws-sdk-go-v2/service/textract"
"github.com/google/uuid"
"github.com/pashagolub/pgxmock/v3"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type DocTextConfig struct {
@@ -37,91 +40,116 @@ func TestCreate(t *testing.T) {
pool, err := pgxmock.NewPool()
require.NoError(t, err)
mockSQS := queuemock.NewMockSQSClient(t)
cfg := &DocTextConfig{}
cfg.QueueClient = mockSQS
cfg.QuerySyncURL = "/i/am/here"
cfg.DBPool = pool
cfg.DBQueries = repository.New(pool)
mockS3 := objectstoremock.NewMockS3Client(t)
cfg.StoreClient = mockS3
mockStore := objectstoremock.NewMockS3Client(t)
cfg.StoreClient = mockStore
mockTextract := textractmock.NewMockTextractClient(t)
cfg.TextractClient = mockTextract
mockQueue := queuemock.NewMockSQSClient(t)
cfg.QueueClient = mockQueue
cfg.QuerySyncURL = "ex"
svc := Service{
cfg: cfg,
}
t.Run("valid", func(t *testing.T) {
docId := uuid.New()
docId := uuid.New()
t.Run("is not extracted", func(t *testing.T) {
cleanId := uuid.New()
bucket := "bucket_name"
key := "/i/am/here"
clientId := "AAA"
hash := "example"
bucket := "bucket"
hash := `"a06d5e0e795adeb6d0b3732330dbcc15"`
textId := uuid.New()
key := objectstore.BucketKey{
ClientID: "aa",
Location: objectstore.Import,
CreatedAt: time.Now().UTC(),
EntityID: cleanId,
}
keyStr := key.String()
textractFile := "../../../assets/sampleGeneration/generated/helloWorld.gen"
textractBaseOut := getTextractFileResponse(t, textractFile)
mimetype := repository.NullCleanmimetype{
Valid: true,
Cleanmimetype: repository.CleanmimetypeApplicationPdf,
}
pool.ExpectQuery("name: IsDocumentTextExtracted :one").WithArgs(docId).WillReturnRows(
pgxmock.NewRows([]string{"isextracted"}).
AddRow(false),
)
mimeType := "application/pdf"
dbmimetype := repository.NullCleanmimetype{
Valid: true,
Cleanmimetype: repository.Cleanmimetype(mimeType),
}
pool.ExpectQuery("name: GetCleanEntryByDocId :one").WithArgs(docId).WillReturnRows(
pgxmock.NewRows([]string{"id", "documentId", "bucket", "key", "version", "hash", "mimetype", "fail", "externalClientId"}).
AddRow(cleanId, docId, &bucket, &key, int64(1), &hash, dbmimetype, repository.NullCleanfailtype{}, clientId),
)
jobId := "hello"
triggerId := uuid.New()
pool.ExpectBegin()
pool.ExpectQuery("name: GetTextractOutputCurrentPart :one").WithArgs(&clientId, pgxmock.AnyArg()).
pool.ExpectQuery("name: GetCleanEntryByDocId :one").WithArgs(docId).
WillReturnRows(
pgxmock.NewRows([]string{"part", "count"}).
AddRow(0, 2),
pgxmock.NewRows([]string{"id", "documentId", "bucket", "key", "version", "hash", "mimetype", "fail", "clientId"}).
AddRow(cleanId, docId, &bucket, &keyStr, int64(123), &hash, mimetype, nil, key.ClientID),
)
pool.ExpectQuery("name: AddDocumentTextTrigger :one").WithArgs(cleanId, pgxmock.AnyArg(), pgxmock.AnyArg(), uint16(0)).WillReturnRows(
pgxmock.NewRows([]string{"id"}).AddRow(triggerId),
)
mockTextract.EXPECT().
StartDocumentTextDetection(
bodyMsg := io.NopCloser(strings.NewReader(pdfHelloWorld))
mockStore.EXPECT().
GetObject(
mock.Anything,
mock.MatchedBy(func(in *awstextract.StartDocumentTextDetectionInput) bool {
return true
mock.MatchedBy(func(in *s3.GetObjectInput) bool {
return *in.Bucket == bucket && *in.Key == keyStr
}),
mock.Anything,
).
Return(&awstextract.StartDocumentTextDetectionOutput{
JobId: &jobId,
Return(&s3.GetObjectOutput{
Body: bodyMsg,
}, nil)
sent := 0
mockTextract.EXPECT().
AnalyzeDocument(
mock.Anything,
mock.MatchedBy(func(in *awstextract.AnalyzeDocumentInput) bool {
if sent == 0 {
sent++
return true
}
return false
}),
mock.Anything,
).
Return(&textractBaseOut["base"][0], nil).Once()
pool.ExpectExec("name: AddDocumentTextTriggerJobId :exec").WithArgs(&jobId, triggerId).
pool.ExpectBegin()
pool.ExpectQuery("name: GetDocumentTextExtractionByHash :one").WithArgs(cleanId, hash).WillReturnRows(
pgxmock.NewRows([]string{"id"}).
AddRow(textId),
)
pool.ExpectExec("name: AddDocumentTextEntry :exec").WithArgs(textId, pgxmock.AnyArg()).
WillReturnResult(pgxmock.NewResult("", 1))
pool.ExpectCommit()
mockQueue.EXPECT().
SendMessage(
mock.Anything,
mock.MatchedBy(func(in *sqs.SendMessageInput) bool {
return *in.QueueUrl == cfg.QuerySyncURL && *in.MessageBody == fmt.Sprintf("{\"id\":\"%s\"}", docId.String())
}),
mock.Anything,
).
Return(&sqs.SendMessageOutput{}, nil)
err = svc.Extract(ctx, docId)
require.NoError(t, err)
})
t.Run("invalid clean", func(t *testing.T) {
id := uuid.New()
fail := repository.NullCleanfailtype{
Valid: true,
Cleanfailtype: repository.CleanfailtypeInvalidMimetype,
}
pool.ExpectQuery("name: IsDocumentTextExtracted :one").WithArgs(id).WillReturnRows(
pgxmock.NewRows([]string{"isextracted"}).AddRow(false),
)
pool.ExpectQuery("name: GetCleanEntryByDocId :one").WithArgs(id).WillReturnRows(
pgxmock.NewRows([]string{"id", "documentId", "bucket", "key", "version", "hash", "mimetype", "fail", "externalClientId"}).
AddRow(id, uuid.New(), nil, nil, int64(1), nil, nil, fail, "AAA"),
t.Run("is extracted", func(t *testing.T) {
pool.ExpectQuery("name: IsDocumentTextExtracted :one").WithArgs(docId).WillReturnRows(
pgxmock.NewRows([]string{"isextracted"}).
AddRow(true),
)
err = svc.Extract(ctx, id)
mockQueue.EXPECT().
SendMessage(
mock.Anything,
mock.MatchedBy(func(in *sqs.SendMessageInput) bool {
return *in.QueueUrl == cfg.QuerySyncURL && *in.MessageBody == fmt.Sprintf("{\"id\":\"%s\"}", docId.String())
}),
mock.Anything,
).
Return(&sqs.SendMessageOutput{}, nil)
err = svc.Extract(ctx, docId)
require.NoError(t, err)
})
}
+223
View File
@@ -0,0 +1,223 @@
package documenttext
import (
"context"
"fmt"
"log/slog"
"strings"
documenttypes "queryorchestration/internal/document/types"
"github.com/aws/aws-sdk-go-v2/service/textract"
"github.com/aws/aws-sdk-go-v2/service/textract/types"
"github.com/google/uuid"
)
func (s *Service) getPageText(ctx context.Context, document documenttypes.File, index int) (string, error) {
res, err := s.GetBasePage(ctx, document, index)
if err != nil {
return "", err
}
var pageBlock *types.Block
for _, block := range res.Blocks {
if block.BlockType == types.BlockTypePage {
pageBlock = &block
break
}
}
if pageBlock == nil {
return "", fmt.Errorf("no page block found for index: %d", index)
}
blockMap := s.getBlockMap(res.Blocks)
lines, tables, tableBlockMap, found, err := s.getPageElements(ctx, document, index, *pageBlock, blockMap)
if err != nil {
return "", err
}
return s.buildPageText(ctx, lines, document, blockMap, index, tables, tableBlockMap, found)
}
func (s *Service) getPageElements(ctx context.Context, document documenttypes.File, index int, pageBlock types.Block, blockMap map[uuid.UUID]types.Block) ([]uuid.UUID, []uuid.UUID, map[uuid.UUID]types.Block, map[uuid.UUID]*bool, error) {
lines := []uuid.UUID{}
hasTable := false
found := map[uuid.UUID]*bool{}
for _, id := range s.getChildIds(pageBlock) {
if found[id] != nil && !*found[id] {
continue
}
if s.isLayout(blockMap[id].BlockType) {
lines = append(lines, id)
f := true
found[id] = &f
}
if blockMap[id].BlockType == types.BlockTypeLayoutTable {
hasTable = true
}
if blockMap[id].BlockType == types.BlockTypeLayoutTable ||
blockMap[id].BlockType == types.BlockTypeLayoutList {
for _, childId := range s.getChildIds(blockMap[id]) {
f := false
found[childId] = &f
}
}
}
var tables []uuid.UUID
var tableBlockMap map[uuid.UUID]types.Block
if hasTable {
ids, blockMap, err := s.getTables(ctx, document, index)
if err != nil {
return nil, nil, nil, nil, err
}
tables = ids
tableBlockMap = blockMap
}
return lines, tables, tableBlockMap, found, nil
}
func (s *Service) isLayout(blockType types.BlockType) bool {
return blockType == types.BlockTypeLayoutText ||
blockType == types.BlockTypeLayoutTitle ||
blockType == types.BlockTypeLayoutHeader ||
blockType == types.BlockTypeLayoutFooter ||
blockType == types.BlockTypeLayoutSectionHeader ||
blockType == types.BlockTypeLayoutPageNumber ||
blockType == types.BlockTypeLayoutList ||
blockType == types.BlockTypeLayoutFigure ||
blockType == types.BlockTypeLayoutTable ||
blockType == types.BlockTypeLayoutKeyValue ||
blockType == types.BlockTypeSignature
}
type buildParams struct {
numOfSignatures int
currentTable int
}
func (s *Service) buildPageText(ctx context.Context, lines []uuid.UUID, document documenttypes.File, blockMap map[uuid.UUID]types.Block, index int, tables []uuid.UUID, tableBlockMap map[uuid.UUID]types.Block, found map[uuid.UUID]*bool) (string, error) {
var builder strings.Builder
params := buildParams{
numOfSignatures: 0,
currentTable: 0,
}
for _, id := range lines {
if found[id] == nil || !*found[id] {
continue
}
err := s.addComponentText(ctx, blockMap[id], blockMap, tables, tableBlockMap, &params, &builder)
if err != nil {
return "", err
}
}
if params.numOfSignatures > 0 {
builder.WriteString(fmt.Sprintf("\nThis page has %d signature.\n", params.numOfSignatures))
}
return builder.String(), nil
}
func (s *Service) addComponentText(ctx context.Context, block types.Block, blockMap map[uuid.UUID]types.Block, tables []uuid.UUID, tableBlockMap map[uuid.UUID]types.Block, params *buildParams, builder *strings.Builder) error {
switch block.BlockType {
case types.BlockTypeLayoutText, types.BlockTypeLayoutTitle, types.BlockTypeLayoutHeader, types.BlockTypeLayoutFooter, types.BlockTypeLayoutSectionHeader, types.BlockTypeLayoutPageNumber, types.BlockTypeLayoutList, types.BlockTypeLayoutFigure, types.BlockTypeLayoutKeyValue:
for _, id := range s.getChildIds(block) {
err := s.addComponentText(ctx, blockMap[id], blockMap, tables, tableBlockMap, params, builder)
if err != nil {
return err
}
}
case types.BlockTypeLine:
_, err := builder.WriteString(fmt.Sprintf("%s\n", *block.Text))
if err != nil {
slog.Error("unable to write string", "error", err)
return err
}
case types.BlockTypeWord:
_, err := builder.WriteString(fmt.Sprintf(" %s", *block.Text))
if err != nil {
slog.Error("unable to write string", "error", err)
return err
}
case types.BlockTypeSignature:
params.numOfSignatures++
case types.BlockTypeLayoutTable:
tableId := tables[params.currentTable]
params.currentTable++
tableBlock := tableBlockMap[tableId]
err := s.addTable(ctx, tableBlock, tableBlockMap, builder)
if err != nil {
return err
}
}
return nil
}
func (s *Service) GetBasePage(ctx context.Context, doc documenttypes.File, index int) (textract.AnalyzeDocumentOutput, error) {
pageBytes, err := doc.GetPageAsPNG(ctx, index)
if err != nil {
return textract.AnalyzeDocumentOutput{}, err
}
res, err := s.cfg.GetTextractClient().AnalyzeDocument(ctx, &textract.AnalyzeDocumentInput{
Document: &types.Document{
Bytes: pageBytes,
},
FeatureTypes: []types.FeatureType{
types.FeatureTypeLayout,
types.FeatureTypeSignatures,
},
})
if err != nil {
return textract.AnalyzeDocumentOutput{}, err
}
return *res, err
}
func (s *Service) getBlockMap(blocksList []types.Block) map[uuid.UUID]types.Block {
blocks := make(map[uuid.UUID]types.Block, len(blocksList))
for _, block := range blocksList {
if block.Id == nil {
continue
}
id, err := uuid.Parse(*block.Id)
if err != nil {
slog.Warn("error parsing block id", "error", err, "id", *block.Id)
continue
}
blocks[id] = block
}
return blocks
}
func (s *Service) getChildIds(block types.Block) []uuid.UUID {
childIds := []uuid.UUID{}
for _, relationship := range block.Relationships {
if relationship.Type != types.RelationshipTypeChild {
slog.Info("not a child element", "type", relationship.Type)
continue
}
for _, idStr := range relationship.Ids {
id, err := uuid.Parse(idStr)
if err != nil {
slog.Error("invalid child uuid", "id", id, "error", err)
continue
}
childIds = append(childIds, id)
}
}
return childIds
}
+570
View File
@@ -0,0 +1,570 @@
package documenttext
import (
"context"
"encoding/json"
"io"
"math"
"os"
"strings"
"testing"
documenttypes "queryorchestration/internal/document/types"
textractmock "queryorchestration/mocks/textract"
"github.com/aws/aws-sdk-go-v2/service/textract"
"github.com/aws/aws-sdk-go-v2/service/textract/types"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestGetBasePage(t *testing.T) {
ctx := context.Background()
cfg := DocTextConfig{}
svc := Service{
cfg: &cfg,
}
mockTextract := textractmock.NewMockTextractClient(t)
cfg.TextractClient = mockTextract
sent := 0
pdf, err := documenttypes.NewPDFFromReader(strings.NewReader(pdfHelloWorld))
require.NoError(t, err)
textractFile := "../../../assets/sampleGeneration/generated/helloWorld.gen"
textractBaseOut := getTextractFileResponse(t, textractFile)
mockTextract.EXPECT().
AnalyzeDocument(
mock.Anything,
mock.MatchedBy(func(in *textract.AnalyzeDocumentInput) bool {
if sent == 0 {
sent++
return true
}
return false
}),
mock.Anything,
).
Return(&textractBaseOut["base"][0], nil).Once()
text, err := svc.GetBasePage(ctx, pdf, 0)
require.NoError(t, err)
assert.EqualExportedValues(t, textractBaseOut["base"][0], text)
}
func TestGetDocumentText(t *testing.T) {
ctx := context.Background()
cfg := DocTextConfig{}
svc := Service{
cfg: &cfg,
}
t.Run("Hello World", func(t *testing.T) {
mockTextract := textractmock.NewMockTextractClient(t)
cfg.TextractClient = mockTextract
sent := 0
pdf, err := documenttypes.NewPDFFromReader(strings.NewReader(pdfHelloWorld))
require.NoError(t, err)
textractFile := "../../../assets/sampleGeneration/generated/helloWorld.gen"
textractBaseOut := getTextractFileResponse(t, textractFile)
mockTextract.EXPECT().
AnalyzeDocument(
mock.Anything,
mock.MatchedBy(func(in *textract.AnalyzeDocumentInput) bool {
if sent == 0 {
sent++
return true
}
return false
}),
mock.Anything,
).
Return(&textractBaseOut["base"][0], nil).Once()
text, err := svc.getDocumentText(ctx, pdf)
require.NoError(t, err)
expected := `Start of Page No. = 1
Hello World!
`
assertStringToReader(t, expected, text)
})
t.Run("sampleOne - cnc_01-06", func(t *testing.T) {
mockTextract := textractmock.NewMockTextractClient(t)
cfg.TextractClient = mockTextract
sent := 0
textractFile := "../../../assets/sampleOne/generated/cnc_01-06.gen"
textractBaseOut := getTextractFileResponse(t, textractFile)
pdfFile, err := os.Open("../../../assets/sampleOne/cnc_01-06.pdf")
require.NoError(t, err)
defer pdfFile.Close()
pdf, err := documenttypes.NewPDFFromReader(pdfFile)
require.NoError(t, err)
txtFile, err := os.Open("../../../assets/sampleOne/cnc_01-06.txt")
require.NoError(t, err)
defer txtFile.Close()
mockTextract.EXPECT().
AnalyzeDocument(
mock.Anything,
mock.MatchedBy(func(in *textract.AnalyzeDocumentInput) bool {
if sent == 0 {
sent++
return true
}
return false
}),
mock.Anything,
).
Return(&textractBaseOut["base"][0], nil).Once()
mockTextract.EXPECT().
AnalyzeDocument(
mock.Anything,
mock.MatchedBy(func(in *textract.AnalyzeDocumentInput) bool {
if sent == 1 {
sent++
return true
}
return false
}),
mock.Anything,
).
Return(&textractBaseOut["base"][1], nil).Once()
mockTextract.EXPECT().
AnalyzeDocument(
mock.Anything,
mock.MatchedBy(func(in *textract.AnalyzeDocumentInput) bool {
if sent == 2 {
sent++
return true
}
return false
}),
mock.Anything,
).
Return(&textractBaseOut["base"][2], nil).Once()
mockTextract.EXPECT().
AnalyzeDocument(
mock.Anything,
mock.MatchedBy(func(in *textract.AnalyzeDocumentInput) bool {
if sent == 3 {
sent++
return true
}
return false
}),
mock.Anything,
).
Return(&textractBaseOut["base"][3], nil).Once()
text, err := svc.getDocumentText(ctx, pdf)
require.NoError(t, err)
assertReaders(t, txtFile, text)
})
t.Run("sampleOne - hn_0109", func(t *testing.T) {
mockTextract := textractmock.NewMockTextractClient(t)
cfg.TextractClient = mockTextract
sent := 0
textractFile := "../../../assets/sampleOne/generated/hn_0109.gen"
textractBaseOut := getTextractFileResponse(t, textractFile)
pdfFile, err := os.Open("../../../assets/sampleOne/hn_0109.pdf")
require.NoError(t, err)
defer pdfFile.Close()
pdf, err := documenttypes.NewPDFFromReader(pdfFile)
require.NoError(t, err)
txtFile, err := os.Open("../../../assets/sampleOne/hn_0109.txt")
require.NoError(t, err)
defer txtFile.Close()
mockTextract.EXPECT().
AnalyzeDocument(
mock.Anything,
mock.MatchedBy(func(in *textract.AnalyzeDocumentInput) bool {
if sent == 0 {
sent++
return true
}
return false
}),
mock.Anything,
).
Return(&textractBaseOut["base"][0], nil).Once()
mockTextract.EXPECT().
AnalyzeDocument(
mock.Anything,
mock.MatchedBy(func(in *textract.AnalyzeDocumentInput) bool {
if sent == 1 {
sent++
return true
}
return false
}),
mock.Anything,
).
Return(&textractBaseOut["base"][1], nil).Once()
mockTextract.EXPECT().
AnalyzeDocument(
mock.Anything,
mock.MatchedBy(func(in *textract.AnalyzeDocumentInput) bool {
if sent == 2 && in.FeatureTypes[0] == types.FeatureTypeTables {
sent++
return true
}
return false
}),
mock.Anything,
).
Return(&textractBaseOut["table"][0], nil).Once()
mockTextract.EXPECT().
AnalyzeDocument(
mock.Anything,
mock.MatchedBy(func(in *textract.AnalyzeDocumentInput) bool {
if sent == 3 {
sent++
return true
}
return false
}),
mock.Anything,
).
Return(&textractBaseOut["base"][2], nil).Once()
mockTextract.EXPECT().
AnalyzeDocument(
mock.Anything,
mock.MatchedBy(func(in *textract.AnalyzeDocumentInput) bool {
if sent == 4 && in.FeatureTypes[0] == types.FeatureTypeTables {
sent++
return true
}
return false
}),
mock.Anything,
).
Return(&textractBaseOut["table"][1], nil).Once()
mockTextract.EXPECT().
AnalyzeDocument(
mock.Anything,
mock.MatchedBy(func(in *textract.AnalyzeDocumentInput) bool {
if sent == 5 {
sent++
return true
}
return false
}),
mock.Anything,
).
Return(&textractBaseOut["base"][3], nil).Once()
text, err := svc.getDocumentText(ctx, pdf)
require.NoError(t, err)
assertReaders(t, txtFile, text)
})
}
func TestGetBlockMap(t *testing.T) {
svc := Service{}
t.Run("0 blocks", func(t *testing.T) {
blocks := []types.Block{}
outBlocks := svc.getBlockMap(blocks)
assert.Equal(t, map[uuid.UUID]types.Block{}, outBlocks)
})
t.Run("nil id blocks", func(t *testing.T) {
blocks := []types.Block{
{},
}
outBlocks := svc.getBlockMap(blocks)
assert.Equal(t, map[uuid.UUID]types.Block{}, outBlocks)
})
t.Run("1 block", func(t *testing.T) {
blockId := uuid.New()
blockIdStr := blockId.String()
blocks := []types.Block{
{
Id: &blockIdStr,
},
}
outBlocks := svc.getBlockMap(blocks)
assert.Equal(t, map[uuid.UUID]types.Block{
blockId: {
Id: &blockIdStr,
},
}, outBlocks)
})
t.Run("invalid block id", func(t *testing.T) {
blockIdStr := "blockId.String()"
blocks := []types.Block{
{
Id: &blockIdStr,
},
}
outBlocks := svc.getBlockMap(blocks)
assert.Equal(t, map[uuid.UUID]types.Block{}, outBlocks)
})
t.Run("2 blocks", func(t *testing.T) {
blockOneId := uuid.New()
blockOneIdStr := blockOneId.String()
blockTwoId := uuid.New()
blockTwoIdStr := blockTwoId.String()
blocks := []types.Block{
{
Id: &blockOneIdStr,
},
{
Id: &blockTwoIdStr,
},
}
outBlocks := svc.getBlockMap(blocks)
assert.Equal(t, map[uuid.UUID]types.Block{
blockOneId: {
Id: &blockOneIdStr,
},
blockTwoId: {
Id: &blockTwoIdStr,
},
}, outBlocks)
})
}
func TestMergeDocument(t *testing.T) {
svc := Service{}
t.Run("0 pages", func(t *testing.T) {
pages := []string{}
out, err := svc.mergeDocument(pages)
require.NoError(t, err)
outStr, err := io.ReadAll(out)
require.NoError(t, err)
assert.Equal(t, ``, string(outStr))
})
t.Run("1 page", func(t *testing.T) {
pages := []string{
"page one",
}
out, err := svc.mergeDocument(pages)
require.NoError(t, err)
outStr, err := io.ReadAll(out)
require.NoError(t, err)
assert.Equal(t, `Start of Page No. = 1
page one
`, string(outStr))
})
t.Run("2 pages", func(t *testing.T) {
pages := []string{
"page one",
"page two",
}
out, err := svc.mergeDocument(pages)
require.NoError(t, err)
outStr, err := io.ReadAll(out)
require.NoError(t, err)
assert.Equal(t, `Start of Page No. = 1
page one
Start of Page No. = 2
page two
`, string(outStr))
})
t.Run("3 pages", func(t *testing.T) {
pages := []string{
"page one",
"page two",
"page three",
}
out, err := svc.mergeDocument(pages)
require.NoError(t, err)
outStr, err := io.ReadAll(out)
require.NoError(t, err)
assert.Equal(t, `Start of Page No. = 1
page one
Start of Page No. = 2
page two
Start of Page No. = 3
page three
`, string(outStr))
})
}
func getTextractFileResponse(t *testing.T, filename string) map[string][]textract.AnalyzeDocumentOutput {
file, err := os.Open(filename)
require.NoError(t, err)
defer file.Close()
decoder := json.NewDecoder(file)
var out map[string][]textract.AnalyzeDocumentOutput
err = decoder.Decode(&out)
require.NoError(t, err)
return out
}
func assertStringToReader(t *testing.T, expected string, actual io.Reader) {
actualBytes, err := io.ReadAll(actual)
require.NoError(t, err)
actualStr := string(actualBytes)
if !assert.Equal(t, expected, actualStr) {
diff := cmp.Diff(expected, actualStr)
t.Logf("Detailed diff (-expected +actual):\n%s", diff)
}
}
func assertReaders(t *testing.T, expected io.Reader, actual io.Reader) {
actualBytes, err := io.ReadAll(actual)
require.NoError(t, err)
actualStr := string(actualBytes)
expectedBytes, err := io.ReadAll(expected)
require.NoError(t, err)
expectedStr := string(expectedBytes)
if !assert.Equal(t, expectedStr, actualStr) {
diff := cmp.Diff(expectedStr, actualStr)
t.Logf("Detailed diff (-expected +actual):\n%s", diff)
}
}
func levenshteinDistance(s1, s2 string) int {
rows, cols := len(s1)+1, len(s2)+1
dist := make([][]int, rows)
for i := range dist {
dist[i] = make([]int, cols)
}
for i := 0; i < rows; i++ {
dist[i][0] = i
}
for j := 0; j < cols; j++ {
dist[0][j] = j
}
for i := 1; i < rows; i++ {
for j := 1; j < cols; j++ {
cost := 1
if s1[i-1] == s2[j-1] {
cost = 0
}
dist[i][j] = min(
dist[i-1][j]+1,
dist[i][j-1]+1,
dist[i-1][j-1]+cost,
)
}
}
return dist[rows-1][cols-1]
}
func calculateSimilarityPercentage(s1, s2 string) float64 {
distance := levenshteinDistance(s1, s2)
maxLen := math.Max(float64(len(s1)), float64(len(s2)))
if maxLen == 0 {
return 100.0
}
return 100.0 * (1.0 - float64(distance)/maxLen)
}
func AreSimilar(s1, s2 string, threshold float64) bool {
return calculateSimilarityPercentage(s1, s2) >= threshold
}
const pdfHelloWorld = `%PDF-1.4
%
1 0 obj
<<
/Type /Catalog
/Pages 2 0 R
/Version /1.4
>>
endobj
2 0 obj
<<
/Type /Pages
/Kids [3 0 R]
/Count 1
>>
endobj
3 0 obj
<<
/Type /Page
/Parent 2 0 R
/MediaBox [0 0 612 792]
/Resources <<
/Font <<
/F1 <<
/Type /Font
/Subtype /Type1
/BaseFont /Helvetica
>>
>>
>>
/Contents 4 0 R
>>
endobj
4 0 obj
<<
/Length
44
>>
stream
BT
/F1 24 Tf
100 700 Td
(Hello World!) Tj
ET
endstream
endobj
xref
0 5
0000000000 65535 f
0000000015 00000 n
0000000086 00000 n
0000000151 00000 n
0000000376 00000 n
trailer
<<
/Size 5
/Root 1 0 R
>>
startxref
472
%%EOF`
@@ -1,292 +0,0 @@
package documenttext
import (
"context"
"database/sql"
"fmt"
"io"
"strings"
"testing"
"time"
"queryorchestration/internal/database/repository"
"queryorchestration/internal/serviceconfig/objectstore"
objectstoremock "queryorchestration/mocks/objectstore"
queuemock "queryorchestration/mocks/queue"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/google/uuid"
"github.com/pashagolub/pgxmock/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestProcess(t *testing.T) {
ctx := context.Background()
pool, err := pgxmock.NewPool()
require.NoError(t, err)
cfg := &DocTextConfig{}
cfg.DBPool = pool
cfg.DBQueries = repository.New(pool)
cfg.QuerySyncURL = "querysync"
mockSQS := queuemock.NewMockSQSClient(t)
cfg.QueueClient = mockSQS
mockS3 := objectstoremock.NewMockS3Client(t)
cfg.StoreClient = mockS3
svc := Service{
cfg: cfg,
}
triggerId := uuid.MustParse("6ac21b2b-f36c-4ae7-a815-307d4a9bfe4d")
docId := uuid.MustParse("6ac21b2b-f36c-4ae7-a815-307d4a9bfe4a")
textractId := "hello"
textId := uuid.New()
cleanId := uuid.New()
hash := `"5d41402abc4b2a76b9719d911017c592"`
pool.ExpectQuery("name: GetDocumentTextTrigger :one").WithArgs(triggerId).WillReturnRows(
pgxmock.NewRows([]string{"id", "cleanId", "version", "textractJobId", "documentId"}).
AddRow(triggerId, cleanId, int64(123), &textractId, docId),
)
pool.ExpectBegin()
pool.ExpectQuery("name: GetDocumentTextExtractionByHash :one").WithArgs(cleanId, hash).WillReturnRows(
pgxmock.NewRows([]string{"id"}).
AddRow(textId),
)
pool.ExpectExec("name: AddDocumentTextEntry :exec").WithArgs(textId, triggerId, pgxmock.AnyArg()).
WillReturnResult(pgxmock.NewResult("", 1))
pool.ExpectCommit()
body := io.NopCloser(strings.NewReader("hello"))
mockS3.EXPECT().
GetObject(
mock.Anything,
mock.MatchedBy(func(in *s3.GetObjectInput) bool {
return *in.IfMatch == "hash"
}),
mock.Anything,
).
Return(&s3.GetObjectOutput{
Body: body,
}, nil)
mockSQS.EXPECT().
SendMessage(
mock.Anything,
mock.MatchedBy(func(in *sqs.SendMessageInput) bool {
return *in.QueueUrl == cfg.QuerySyncURL && *in.MessageBody == fmt.Sprintf("{\"id\":\"%s\"}", docId.String())
}),
mock.Anything,
).
Return(&sqs.SendMessageOutput{}, nil)
key := objectstore.BucketKey{
ClientID: "my_client",
Location: objectstore.TextTextract,
EntityID: triggerId,
}
err = svc.Process(ctx, &ProcessParams{
Bucket: "bucket",
Key: key,
Hash: "hash",
})
assert.NoError(t, err)
}
func TestStoreTrigger(t *testing.T) {
ctx := context.Background()
pool, err := pgxmock.NewPool()
require.NoError(t, err)
cfg := &DocTextConfig{}
cfg.DBPool = pool
cfg.DBQueries = repository.New(pool)
cfg.QuerySyncURL = "querysync"
mockSQS := queuemock.NewMockSQSClient(t)
cfg.QueueClient = mockSQS
mockS3 := objectstoremock.NewMockS3Client(t)
cfg.StoreClient = mockS3
svc := Service{
cfg: cfg,
}
cleanId := uuid.New()
textId := uuid.New()
triggerId := uuid.New()
clientId := "hello"
bucket := "bucket"
t.Run("exists", func(t *testing.T) {
hash := `"5d41402abc4b2a76b9719d911017c592"`
pool.ExpectBegin()
pool.ExpectQuery("name: GetDocumentTextExtractionByHash :one").WithArgs(cleanId, hash).WillReturnRows(
pgxmock.NewRows([]string{"id"}).
AddRow(textId),
)
pool.ExpectExec("name: AddDocumentTextEntry :exec").WithArgs(textId, triggerId, pgxmock.AnyArg()).
WillReturnResult(pgxmock.NewResult("", 1))
pool.ExpectCommit()
text := strings.NewReader("hello")
err = svc.storeProcess(ctx, text, &repository.GetDocumentTextTriggerRow{
ID: triggerId,
Cleanid: cleanId,
}, &ProcessParams{
Bucket: bucket,
Key: objectstore.BucketKey{},
})
require.NoError(t, err)
})
t.Run("not exists", func(t *testing.T) {
part := uint16(0)
key := objectstore.BucketKey{
ClientID: clientId,
Location: objectstore.TextOut,
CreatedAt: time.Now().UTC(),
EntityID: triggerId,
Part: &part,
}
hash := `"09644372e99020106946045c6fd2d70b"`
pool.ExpectBegin()
pool.ExpectQuery("name: GetDocumentTextExtractionByHash :one").WithArgs(cleanId, hash).
WillReturnError(sql.ErrNoRows)
pool.ExpectQuery("name: GetTextOutCurrentPart :one").WithArgs(&clientId, pgxmock.AnyArg()).
WillReturnRows(
pgxmock.NewRows([]string{"part", "count"}).
AddRow(0, 2),
)
pool.ExpectQuery("name: AddDocumentText :one").WithArgs(bucket, key.String(), hash, pgxmock.AnyArg(), uint16(0)).
WillReturnRows(
pgxmock.NewRows([]string{"id"}).
AddRow(textId),
)
mockS3.EXPECT().
PutObject(
mock.Anything,
mock.MatchedBy(func(in *s3.PutObjectInput) bool {
bod, err := io.ReadAll(in.Body)
require.NoError(t, err)
return string(bod) == "byebye" && *in.Key == key.String() && *in.Bucket == bucket
}),
mock.Anything,
).
Return(&s3.PutObjectOutput{}, nil)
pool.ExpectExec("name: AddDocumentTextEntry :exec").WithArgs(textId, triggerId, pgxmock.AnyArg()).
WillReturnResult(pgxmock.NewResult("", 1))
pool.ExpectCommit()
text := strings.NewReader("byebye")
err = svc.storeProcess(ctx, text, &repository.GetDocumentTextTriggerRow{
ID: triggerId,
Cleanid: cleanId,
}, &ProcessParams{
Key: key,
Bucket: bucket,
})
require.NoError(t, err)
})
}
func TestDuplicateReader(t *testing.T) {
original := strings.NewReader("hello")
readerOne, readerTwo, err := duplicateReader(original)
require.NoError(t, err)
contentOne, err := io.ReadAll(readerOne)
require.NoError(t, err)
assert.Equal(t, []byte("hello"), contentOne)
contentTwo, err := io.ReadAll(readerTwo)
require.NoError(t, err)
assert.Equal(t, []byte("hello"), contentTwo)
}
func TestProcessTrigger(t *testing.T) {
ctx := context.Background()
pool, err := pgxmock.NewPool()
require.NoError(t, err)
cfg := &DocTextConfig{}
cfg.DBPool = pool
cfg.DBQueries = repository.New(pool)
cfg.QuerySyncURL = "querysync"
mockSQS := queuemock.NewMockSQSClient(t)
cfg.QueueClient = mockSQS
mockS3 := objectstoremock.NewMockS3Client(t)
cfg.StoreClient = mockS3
svc := Service{
cfg: cfg,
}
cleanId := uuid.New()
textId := uuid.New()
triggerId := uuid.New()
clientId := "hello"
bucket := "bucket"
key := objectstore.BucketKey{
ClientID: clientId,
Location: objectstore.TextOut,
EntityID: triggerId,
CreatedAt: time.Now().UTC(),
}
hash := `"5d41402abc4b2a76b9719d911017c592"`
pool.ExpectBegin()
pool.ExpectQuery("name: GetDocumentTextExtractionByHash :one").WithArgs(cleanId, hash).
WillReturnError(sql.ErrNoRows)
pool.ExpectQuery("name: GetTextOutCurrentPart :one").WithArgs(&clientId, pgxmock.AnyArg()).
WillReturnRows(
pgxmock.NewRows([]string{"part", "count"}).
AddRow(0, 2),
)
pool.ExpectQuery("name: AddDocumentText :one").WithArgs(bucket, key.String(), hash, pgxmock.AnyArg(), uint16(0)).
WillReturnRows(
pgxmock.NewRows([]string{"id"}).
AddRow(textId),
)
body := io.NopCloser(strings.NewReader("hello"))
mockS3.EXPECT().
GetObject(
mock.Anything,
mock.MatchedBy(func(in *s3.GetObjectInput) bool {
return *in.Key == key.String() && *in.Bucket == bucket && *in.IfMatch == hash
}),
mock.Anything,
).
Return(&s3.GetObjectOutput{
Body: body,
}, nil)
mockS3.EXPECT().
PutObject(
mock.Anything,
mock.MatchedBy(func(in *s3.PutObjectInput) bool {
bod, err := io.ReadAll(in.Body)
require.NoError(t, err)
return string(bod) == "hello" && *in.Key == key.String() && *in.Bucket == bucket
}),
mock.Anything,
).
Return(&s3.PutObjectOutput{}, nil)
pool.ExpectExec("name: AddDocumentTextEntry :exec").WithArgs(textId, triggerId, pgxmock.AnyArg()).
WillReturnResult(pgxmock.NewResult("", 1))
pool.ExpectCommit()
err = svc.processTrigger(ctx, &repository.GetDocumentTextTriggerRow{
ID: triggerId,
Cleanid: cleanId,
}, &ProcessParams{
Bucket: bucket,
Key: key,
Hash: hash,
})
require.NoError(t, err)
}
@@ -6,7 +6,6 @@ import (
"database/sql"
"errors"
"io"
"strings"
"time"
"queryorchestration/internal/database/repository"
@@ -18,62 +17,7 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
type ProcessParams struct {
Bucket string
Key objectstore.BucketKey
Hash string
}
type Page string
func (s *Service) getPages(baseTextract io.Reader) ([]*Page, error) {
// For each page
// Look for table, form, signature
// If one/many found - run appropriate tier and merge in
// tiers?? - analyze (for tables?)
// cleanPages
// linkPages
return []*Page{}, nil
}
func (s *Service) mergePages(pages []*Page) (io.Reader, error) {
// merge document - pages to text
// e.g. common tables
return strings.NewReader("hello"), nil
}
func (s *Service) processTrigger(ctx context.Context, trigger *repository.GetDocumentTextTriggerRow, params *ProcessParams) error {
keyStr := params.Key.String()
response, err := s.cfg.GetStoreClient().GetObject(ctx, &s3.GetObjectInput{
Bucket: &params.Bucket,
Key: &keyStr,
IfMatch: &params.Hash,
})
if err != nil {
return err
}
pages, err := s.getPages(response.Body)
if err != nil {
return err
}
text, err := s.mergePages(pages)
if err != nil {
return err
}
err = s.storeProcess(ctx, text, trigger, params)
if err != nil {
return err
}
return nil
}
func (s *Service) storeProcess(ctx context.Context, text io.Reader, trigger *repository.GetDocumentTextTriggerRow, params *ProcessParams) error {
func (s *Service) storeProcess(ctx context.Context, text io.Reader, clean *repository.Currentcleanentry) error {
version := build.GetVersionUnixTimestamp()
hashReader, storeReader, err := duplicateReader(text)
@@ -88,7 +32,7 @@ func (s *Service) storeProcess(ctx context.Context, text io.Reader, trigger *rep
return s.cfg.ExecuteDBTransaction(ctx, func(ctx context.Context, q *repository.Queries) error {
existingId, err := q.GetDocumentTextExtractionByHash(ctx, &repository.GetDocumentTextExtractionByHashParams{
Cleanentryid: trigger.Cleanid,
Cleanentryid: clean.ID,
Hash: hash,
})
if err != nil && !errors.Is(err, sql.ErrNoRows) {
@@ -99,10 +43,10 @@ func (s *Service) storeProcess(ctx context.Context, text io.Reader, trigger *rep
if existingId == uuid.Nil || errors.Is(err, sql.ErrNoRows) {
createdAt := time.Now().UTC()
key := objectstore.BucketKey{
ClientID: params.Key.ClientID,
Location: objectstore.TextOut,
ClientID: clean.Clientid,
Location: objectstore.Text,
CreatedAt: createdAt,
EntityID: trigger.ID,
EntityID: clean.ID,
}
currentPart, err := q.GetTextOutCurrentPart(ctx, &repository.GetTextOutCurrentPartParams{
@@ -122,10 +66,11 @@ func (s *Service) storeProcess(ctx context.Context, text io.Reader, trigger *rep
keyStr := key.String()
textId, err = q.AddDocumentText(ctx, &repository.AddDocumentTextParams{
Bucket: params.Bucket,
Key: keyStr,
Hash: hash,
Part: *key.Part,
Cleanid: clean.ID,
Bucket: *clean.Bucket,
Key: keyStr,
Hash: hash,
Part: *key.Part,
Createdat: pgtype.Timestamp{
Time: key.CreatedAt,
Valid: true,
@@ -136,7 +81,7 @@ func (s *Service) storeProcess(ctx context.Context, text io.Reader, trigger *rep
}
_, err = s.cfg.GetStoreClient().PutObject(ctx, &s3.PutObjectInput{
Bucket: &params.Bucket,
Bucket: clean.Bucket,
Key: &keyStr,
Body: storeReader,
})
@@ -149,9 +94,8 @@ func (s *Service) storeProcess(ctx context.Context, text io.Reader, trigger *rep
}
err = q.AddDocumentTextEntry(ctx, &repository.AddDocumentTextEntryParams{
Textid: textId,
Triggerid: trigger.ID,
Version: version,
Textid: textId,
Version: version,
})
if err != nil {
return err
@@ -161,25 +105,6 @@ func (s *Service) storeProcess(ctx context.Context, text io.Reader, trigger *rep
})
}
func (s *Service) Process(ctx context.Context, params *ProcessParams) error {
trigger, err := s.cfg.GetDBQueries().GetDocumentTextTrigger(ctx, params.Key.EntityID)
if err != nil {
return err
}
err = s.processTrigger(ctx, trigger, params)
if err != nil {
return err
}
err = s.informExtraction(ctx, trigger.Documentid)
if err != nil {
return err
}
return nil
}
func duplicateReader(reader io.Reader) (io.Reader, io.Reader, error) {
content, err := io.ReadAll(reader)
if err != nil {
+126
View File
@@ -0,0 +1,126 @@
package documenttext
import (
"context"
"database/sql"
"io"
"strings"
"testing"
"time"
"queryorchestration/internal/database/repository"
"queryorchestration/internal/serviceconfig/objectstore"
objectstoremock "queryorchestration/mocks/objectstore"
queuemock "queryorchestration/mocks/queue"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/google/uuid"
"github.com/pashagolub/pgxmock/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestStoreExtract(t *testing.T) {
ctx := context.Background()
pool, err := pgxmock.NewPool()
require.NoError(t, err)
cfg := &DocTextConfig{}
cfg.DBPool = pool
cfg.DBQueries = repository.New(pool)
cfg.QuerySyncURL = "querysync"
mockSQS := queuemock.NewMockSQSClient(t)
cfg.QueueClient = mockSQS
mockS3 := objectstoremock.NewMockS3Client(t)
cfg.StoreClient = mockS3
svc := Service{
cfg: cfg,
}
cleanId := uuid.New()
textId := uuid.New()
clientId := "hello"
bucket := "bucket"
t.Run("exists", func(t *testing.T) {
hash := `"5d41402abc4b2a76b9719d911017c592"`
pool.ExpectBegin()
pool.ExpectQuery("name: GetDocumentTextExtractionByHash :one").WithArgs(cleanId, hash).WillReturnRows(
pgxmock.NewRows([]string{"id"}).
AddRow(textId),
)
pool.ExpectExec("name: AddDocumentTextEntry :exec").WithArgs(textId, pgxmock.AnyArg()).
WillReturnResult(pgxmock.NewResult("", 1))
pool.ExpectCommit()
text := strings.NewReader("hello")
err = svc.storeProcess(ctx, text, &repository.Currentcleanentry{
ID: cleanId,
})
require.NoError(t, err)
})
t.Run("not exists", func(t *testing.T) {
part := uint16(0)
key := objectstore.BucketKey{
ClientID: clientId,
Location: objectstore.Text,
CreatedAt: time.Now().UTC(),
EntityID: cleanId,
Part: &part,
}
keyStr := key.String()
hash := `"09644372e99020106946045c6fd2d70b"`
pool.ExpectBegin()
pool.ExpectQuery("name: GetDocumentTextExtractionByHash :one").WithArgs(cleanId, hash).
WillReturnError(sql.ErrNoRows)
pool.ExpectQuery("name: GetTextOutCurrentPart :one").WithArgs(&clientId, pgxmock.AnyArg()).
WillReturnRows(
pgxmock.NewRows([]string{"part", "count"}).
AddRow(0, 2),
)
pool.ExpectQuery("name: AddDocumentText :one").WithArgs(cleanId, bucket, key.String(), hash, pgxmock.AnyArg(), uint16(0)).
WillReturnRows(
pgxmock.NewRows([]string{"id"}).
AddRow(textId),
)
mockS3.EXPECT().
PutObject(
mock.Anything,
mock.MatchedBy(func(in *s3.PutObjectInput) bool {
bod, err := io.ReadAll(in.Body)
require.NoError(t, err)
return string(bod) == "byebye" && *in.Key == key.String() && *in.Bucket == bucket
}),
mock.Anything,
).
Return(&s3.PutObjectOutput{}, nil)
pool.ExpectExec("name: AddDocumentTextEntry :exec").WithArgs(textId, pgxmock.AnyArg()).
WillReturnResult(pgxmock.NewResult("", 1))
pool.ExpectCommit()
text := strings.NewReader("byebye")
err = svc.storeProcess(ctx, text, &repository.Currentcleanentry{
ID: cleanId,
Clientid: clientId,
Bucket: &bucket,
Key: &keyStr,
})
require.NoError(t, err)
})
}
func TestDuplicateReader(t *testing.T) {
original := strings.NewReader("hello")
readerOne, readerTwo, err := duplicateReader(original)
require.NoError(t, err)
contentOne, err := io.ReadAll(readerOne)
require.NoError(t, err)
assert.Equal(t, []byte("hello"), contentOne)
contentTwo, err := io.ReadAll(readerTwo)
require.NoError(t, err)
assert.Equal(t, []byte("hello"), contentTwo)
}
+109
View File
@@ -0,0 +1,109 @@
package documenttext
import (
"context"
"encoding/json"
"log/slog"
"strings"
documenttypes "queryorchestration/internal/document/types"
"github.com/aws/aws-sdk-go-v2/service/textract"
"github.com/aws/aws-sdk-go-v2/service/textract/types"
"github.com/google/uuid"
)
type cell struct {
value string
row int
column int
}
func (s *Service) addTable(ctx context.Context, block types.Block, blockMap map[uuid.UUID]types.Block, builder *strings.Builder) error {
table := []cell{}
maxRow := 0
maxColumn := 0
for _, uid := range s.getChildIds(block) {
childBlock := blockMap[uid]
if childBlock.BlockType != types.BlockTypeCell {
continue
}
var str strings.Builder
for _, uid := range s.getChildIds(childBlock) {
if blockMap[uid].BlockType == types.BlockTypeWord {
if str.String() != "" {
str.WriteString(" ")
}
str.WriteString(*blockMap[uid].Text)
}
if maxRow < int(*childBlock.RowIndex) {
maxRow = int(*childBlock.RowIndex)
}
if maxColumn < int(*childBlock.ColumnIndex) {
maxColumn = int(*childBlock.ColumnIndex)
}
table = append(table, cell{
column: int(*childBlock.ColumnIndex) - 1,
row: int(*childBlock.RowIndex) - 1,
value: str.String(),
})
}
}
tableArr := make([][]string, maxRow)
for i := range tableArr {
tableArr[i] = make([]string, maxColumn)
}
for _, cell := range table {
tableArr[cell.row][cell.column] = cell.value
}
jsonData, err := json.Marshal(tableArr)
if err != nil {
return err
}
builder.WriteString("-------Table Start--------\n")
builder.WriteString(string(jsonData))
builder.WriteString("\n-------Table End--------\n")
return nil
}
func (s *Service) getTables(ctx context.Context, document documenttypes.File, index int) ([]uuid.UUID, map[uuid.UUID]types.Block, error) {
pageBytes, err := document.GetPageAsPNG(ctx, index)
if err != nil {
return nil, nil, err
}
res, err := s.cfg.GetTextractClient().AnalyzeDocument(ctx, &textract.AnalyzeDocumentInput{
Document: &types.Document{
Bytes: pageBytes,
},
FeatureTypes: []types.FeatureType{
types.FeatureTypeTables,
},
})
if err != nil {
return nil, nil, err
}
tables := []uuid.UUID{}
for _, block := range res.Blocks {
if block.BlockType == types.BlockTypeTable {
id, err := uuid.Parse(*block.Id)
if err != nil {
slog.Warn("error parsing block id", "error", err, "id", *block.Id)
continue
}
tables = append(tables, id)
}
}
blocks := s.getBlockMap(res.Blocks)
return tables, blocks, nil
}
-87
View File
@@ -1,87 +0,0 @@
package documenttext
import (
"context"
"errors"
"time"
"queryorchestration/internal/database/repository"
"queryorchestration/internal/serviceconfig/build"
"queryorchestration/internal/serviceconfig/objectstore"
"github.com/aws/aws-sdk-go-v2/service/textract"
"github.com/aws/aws-sdk-go-v2/service/textract/types"
"github.com/jackc/pgx/v5/pgtype"
)
func (s *Service) triggerExtract(ctx context.Context, clean *repository.Currentcleanentry) error {
version := build.GetVersionUnixTimestamp()
return s.cfg.ExecuteDBTransaction(ctx, func(ctx context.Context, q *repository.Queries) error {
key := objectstore.BucketKey{
CreatedAt: time.Now().UTC(),
Location: objectstore.TextTextract,
ClientID: clean.Clientid,
}
currentPart, err := q.GetTextractOutputCurrentPart(ctx, &repository.GetTextractOutputCurrentPartParams{
Clientid: &clean.Clientid,
Querydate: pgtype.Timestamptz{
Time: key.CreatedAt,
Valid: true,
},
})
if err != nil {
return err
}
part := s.cfg.GetDirectoryPart(currentPart.Part, currentPart.Count)
key.Part = &part
triggerId, err := s.cfg.GetDBQueries().AddDocumentTextTrigger(ctx, &repository.AddDocumentTextTriggerParams{
Cleanid: clean.ID,
Version: version,
Createdat: pgtype.Timestamp{
Time: key.CreatedAt,
Valid: true,
},
Part: *key.Part,
})
if err != nil {
return err
}
key.EntityID = triggerId
keyStr := key.String()
jobTag := triggerId.String()
res, err := s.cfg.GetTextractClient().StartDocumentTextDetection(ctx, &textract.StartDocumentTextDetectionInput{
DocumentLocation: &types.DocumentLocation{
S3Object: &types.S3Object{
Bucket: clean.Bucket,
Name: clean.Key,
},
},
JobTag: &jobTag,
OutputConfig: &types.OutputConfig{
S3Bucket: clean.Bucket,
S3Prefix: &keyStr,
},
})
if err != nil {
return err
} else if res.JobId == nil {
return errors.New("No job id returned from textract")
}
err = s.cfg.GetDBQueries().AddDocumentTextTriggerJobId(ctx, &repository.AddDocumentTextTriggerJobIdParams{
ID: triggerId,
Textractjobid: res.JobId,
})
if err != nil {
return err
}
return nil
})
}
@@ -1,85 +0,0 @@
package documenttext
import (
"context"
"testing"
"queryorchestration/internal/database/repository"
objectstoremock "queryorchestration/mocks/objectstore"
textractmock "queryorchestration/mocks/textract"
"github.com/aws/aws-sdk-go-v2/service/textract"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/google/uuid"
"github.com/pashagolub/pgxmock/v3"
)
func TestTriggerExtract(t *testing.T) {
ctx := context.Background()
pool, err := pgxmock.NewPool()
require.NoError(t, err)
cfg := &DocTextConfig{}
cfg.DBPool = pool
cfg.DBQueries = repository.New(pool)
mockS3 := objectstoremock.NewMockS3Client(t)
cfg.StoreClient = mockS3
mockTextract := textractmock.NewMockTextractClient(t)
cfg.TextractClient = mockTextract
svc := Service{
cfg: cfg,
}
t.Run("successful clean", func(t *testing.T) {
cleanId := uuid.New()
bucket := "bucket_name"
key := "/i/am/here"
hash := "hhasshhh"
clientId := "AA"
jobId := "hello"
triggerId := uuid.New()
pool.ExpectBegin()
pool.ExpectQuery("name: GetTextractOutputCurrentPart :one").WithArgs(&clientId, pgxmock.AnyArg()).
WillReturnRows(
pgxmock.NewRows([]string{"part", "count"}).
AddRow(0, 2),
)
pool.ExpectQuery("name: AddDocumentTextTrigger :one").WithArgs(cleanId, pgxmock.AnyArg(), pgxmock.AnyArg(), uint16(0)).WillReturnRows(
pgxmock.NewRows([]string{"id"}).AddRow(triggerId),
)
mockTextract.EXPECT().
StartDocumentTextDetection(
mock.Anything,
mock.MatchedBy(func(in *textract.StartDocumentTextDetectionInput) bool {
return true
}),
mock.Anything,
).
Return(&textract.StartDocumentTextDetectionOutput{
JobId: &jobId,
}, nil)
pool.ExpectExec("name: AddDocumentTextTriggerJobId :exec").WithArgs(&jobId, triggerId).
WillReturnResult(pgxmock.NewResult("", 1))
pool.ExpectCommit()
err = svc.triggerExtract(ctx, &repository.Currentcleanentry{
ID: cleanId,
Clientid: clientId,
Key: &key,
Bucket: &bucket,
Hash: &hash,
Mimetype: repository.NullCleanmimetype{
Valid: true,
Cleanmimetype: repository.CleanmimetypeApplicationPdf,
},
})
require.NoError(t, err)
})
}
+49
View File
@@ -1,5 +1,14 @@
package documenttypes
import (
"context"
"fmt"
"queryorchestration/internal/serviceconfig/objectstore"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
type MimeType string
const (
@@ -7,3 +16,43 @@ const (
MimeTypeBinaryOctetStream MimeType = "binary/octet-stream"
MimeTypeInvalid MimeType = "invalid"
)
type File interface {
IsCorrupt(context.Context) *InvalidDocumentReason
GetPageAsPNG(context.Context, int) ([]byte, error)
GetPage(context.Context, int) ([]byte, error)
GetPageCount(context.Context) (int, error)
}
type GetFileParams struct {
Bucket string
Key objectstore.BucketKey
Hash string
Mimetype MimeType
}
func GetFile(ctx context.Context, cfg objectstore.ConfigProvider, params GetFileParams) (File, error) {
keyStr := params.Key.String()
resp, err := cfg.GetStoreClient().GetObject(ctx, &s3.GetObjectInput{
Bucket: &params.Bucket,
Key: &keyStr,
IfMatch: &params.Hash,
})
if err != nil {
return nil, err
}
defer resp.Body.Close()
var file File
switch params.Mimetype {
case MimeTypePDF:
pdf, err := NewPDFFromReader(resp.Body)
if err != nil {
return nil, err
}
file = pdf
default:
return nil, fmt.Errorf("invalid mimetype: %s", params.Mimetype)
}
return file, nil
}
-1
View File
@@ -7,7 +7,6 @@ const (
InvalidDocumentRead InvalidDocumentReason = "invalid_read"
InvalidDocumentReadPages InvalidDocumentReason = "invalid_read_pages"
InvalidDocumentZeroPageCount InvalidDocumentReason = "zero_page_count"
InvalidDocumentLargePageCount InvalidDocumentReason = "large_page_count"
InvalidDocumentLargeFile InvalidDocumentReason = "large_file"
InvalidDocumentSmallDimensions InvalidDocumentReason = "small_dimensions"
InvalidDocumentLargeDimensions InvalidDocumentReason = "large_dimensions"
-4
View File
@@ -30,8 +30,6 @@ func ToDBFailType(t InvalidDocumentReason) repository.Cleanfailtype {
return repository.CleanfailtypeInvalidReadPages
case InvalidDocumentZeroPageCount:
return repository.CleanfailtypeZeroPageCount
case InvalidDocumentLargePageCount:
return repository.CleanfailtypeLargePageCount
case InvalidDocumentLargeFile:
return repository.CleanfailtypeLargeFile
case InvalidDocumentSmallDimensions:
@@ -57,8 +55,6 @@ func ParseDBFailType(t repository.Cleanfailtype) InvalidDocumentReason {
return InvalidDocumentReadPages
case repository.CleanfailtypeZeroPageCount:
return InvalidDocumentZeroPageCount
case repository.CleanfailtypeLargePageCount:
return InvalidDocumentLargePageCount
case repository.CleanfailtypeLargeFile:
return InvalidDocumentLargeFile
case repository.CleanfailtypeSmallDimensions:
@@ -75,9 +75,6 @@ func TestParseDBFailType(t *testing.T) {
t.Run("zero page count", func(t *testing.T) {
assert.Equal(t, repository.CleanfailtypeZeroPageCount, documenttypes.ToDBFailType(documenttypes.InvalidDocumentZeroPageCount))
})
t.Run("large page count", func(t *testing.T) {
assert.Equal(t, repository.CleanfailtypeLargePageCount, documenttypes.ToDBFailType(documenttypes.InvalidDocumentLargePageCount))
})
t.Run("large file", func(t *testing.T) {
assert.Equal(t, repository.CleanfailtypeLargeFile, documenttypes.ToDBFailType(documenttypes.InvalidDocumentLargeFile))
})
@@ -131,9 +128,6 @@ func TestToDBFailType(t *testing.T) {
t.Run("zero page count", func(t *testing.T) {
assert.Equal(t, documenttypes.InvalidDocumentZeroPageCount, documenttypes.ParseDBFailType(repository.CleanfailtypeZeroPageCount))
})
t.Run("large page count", func(t *testing.T) {
assert.Equal(t, documenttypes.InvalidDocumentLargePageCount, documenttypes.ParseDBFailType(repository.CleanfailtypeLargePageCount))
})
t.Run("large file", func(t *testing.T) {
assert.Equal(t, documenttypes.InvalidDocumentLargeFile, documenttypes.ParseDBFailType(repository.CleanfailtypeLargeFile))
})
+83 -31
View File
@@ -3,8 +3,12 @@ package documenttypes
import (
"bytes"
"context"
"fmt"
"io"
"log/slog"
"github.com/gen2brain/go-fitz"
"github.com/pdfcpu/pdfcpu/pkg/api"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types"
@@ -13,24 +17,24 @@ import (
type PDF struct {
pdf io.ReadSeeker
config *model.Configuration
maxPages int
maxBytes int64
maxDimension int
minDimension int
minDPI int
pngDPI int
}
const (
TEXTRACT_SYNC_LIMIT int64 = 10 * 1024 * 1024
TEXTRACT_SYNC_LIMIT int64 = 5 * 1024 * 1024
TEXTRACT_ASYNC_LIMIT int64 = 500 * 1024 * 1024
TEXTRACT_PAGE_LIMIT = 3000
TEXTRACT_MAX_DIMENSION = 10000
TEXTRACT_MIN_DIMENSION = 50
TEXTRACT_MIN_DPI = 72
PDF_DEFAULT_DPI = 72
PNG_DEFAULT_DPI = 72
)
func NewPDFFromReadCloser(buf io.ReadCloser) (*PDF, error) {
func NewPDFFromReader(buf io.Reader) (*PDF, error) {
seek, err := ReaderToSeeker(buf)
if err != nil {
return nil, err
@@ -49,14 +53,69 @@ func NewPDF(buf io.ReadSeeker) *PDF {
return &PDF{
pdf: buf,
config: config,
maxPages: TEXTRACT_PAGE_LIMIT,
maxBytes: TEXTRACT_SYNC_LIMIT,
maxDimension: TEXTRACT_MAX_DIMENSION,
minDimension: TEXTRACT_MIN_DIMENSION,
minDPI: TEXTRACT_MIN_DPI,
pngDPI: PNG_DEFAULT_DPI,
}
}
func (s *PDF) GetPageCount(ctx context.Context) (int, error) {
pdfCtx, err := pdfcpu.ReadWithContext(ctx, s.pdf, s.config)
if err != nil {
return 0, err
}
err = pdfCtx.EnsurePageCount()
if err != nil {
return 0, err
}
return pdfCtx.PageCount, nil
}
func (s *PDF) GetPage(ctx context.Context, index int) ([]byte, error) {
pdfCtx, err := pdfcpu.ReadWithContext(ctx, s.pdf, s.config)
if err != nil {
return nil, err
}
err = pdfCtx.EnsurePageCount()
if err != nil {
return nil, err
}
extractedReader, err := api.ExtractPage(pdfCtx, index+1)
if err != nil {
return nil, err
}
return io.ReadAll(extractedReader)
}
func (s *PDF) GetPageAsPNG(ctx context.Context, index int) ([]byte, error) {
pageBytes, err := s.GetPage(ctx, index)
if err != nil {
return nil, err
}
reader := bytes.NewReader(pageBytes)
fitzdoc, err := fitz.NewFromReader(reader)
if err != nil {
return nil, err
}
defer fitzdoc.Close()
img, err := fitzdoc.ImagePNG(0, float64(s.pngDPI))
if err != nil {
return nil, fmt.Errorf("failed to render page %d: %v", index, err)
}
return img, nil
}
func (s *PDF) IsCorrupt(ctx context.Context) *InvalidDocumentReason {
pdfCtx, err := pdfcpu.ReadWithContext(ctx, s.pdf, s.config)
if err != nil {
@@ -64,34 +123,29 @@ func (s *PDF) IsCorrupt(ctx context.Context) *InvalidDocumentReason {
return &reason
}
if int64(pdfCtx.Read.ReadFileSize()) > s.maxBytes {
reason := InvalidDocumentLargeFile
return &reason
}
docErr := s.isValidPageCount(pdfCtx)
if docErr != nil {
return docErr
}
docErr = s.isValidDimensions(pdfCtx)
if docErr != nil {
return docErr
}
return nil
}
func (s *PDF) isValidPageCount(pdfCtx *model.Context) *InvalidDocumentReason {
err := pdfCtx.EnsurePageCount()
count, err := s.GetPageCount(ctx)
if err != nil {
reason := InvalidDocumentRead
return &reason
}
if pdfCtx.PageCount > s.maxPages {
reason := InvalidDocumentLargePageCount
return &reason
for i := range count {
page, err := s.GetPage(ctx, i)
if err != nil {
slog.Error("unable to get page", "index", i, "error", err)
reason := InvalidDocumentRead
return &reason
}
if int64(len(page)) > s.maxBytes {
reason := InvalidDocumentLargeFile
return &reason
}
}
docErr := s.isValidDimensions(pdfCtx)
if docErr != nil {
return docErr
}
return nil
@@ -160,13 +214,11 @@ func (s *PDF) isValidDPI(mediaBox *types.Rectangle, dims types.Dim) *InvalidDocu
return nil
}
func ReaderToSeeker(readCloser io.ReadCloser) (io.ReadSeeker, error) {
func ReaderToSeeker(readCloser io.Reader) (io.ReadSeeker, error) {
data, err := io.ReadAll(readCloser)
if err != nil {
return nil, err
}
readCloser.Close()
return bytes.NewReader(data), nil
}
+102 -47
View File
@@ -2,7 +2,10 @@ package documenttypes
import (
"context"
"io"
"math"
"os"
"path/filepath"
"strings"
"testing"
@@ -165,27 +168,21 @@ func TestIsValidDimensions(t *testing.T) {
})
}
func TestIsValidPageCount(t *testing.T) {
func TestGetPageCount(t *testing.T) {
ctx := context.Background()
t.Run("valid", func(t *testing.T) {
pdf := NewPDF(strings.NewReader(pdfHelloWorld))
pdfCtx, err := pdfcpu.ReadWithContext(ctx, pdf.pdf, pdf.config)
require.NoError(t, err)
reason := pdf.isValidPageCount(pdfCtx)
assert.Nil(t, reason)
count, err := pdf.GetPageCount(ctx)
assert.NoError(t, err)
assert.Equal(t, 1, count)
})
t.Run("large page count", func(t *testing.T) {
pdf := NewPDF(strings.NewReader(pdfTwoPages))
pdfCtx, err := pdfcpu.ReadWithContext(ctx, pdf.pdf, pdf.config)
require.NoError(t, err)
pdf.maxPages = 1
reason := pdf.isValidPageCount(pdfCtx)
assert.Equal(t, InvalidDocumentLargePageCount, *reason)
count, err := pdf.GetPageCount(ctx)
assert.NoError(t, err)
assert.Equal(t, 2, count)
})
}
@@ -217,13 +214,6 @@ func TestIsCorrupt(t *testing.T) {
reason := pdf.IsCorrupt(ctx)
assert.Equal(t, InvalidDocumentLargeDimensions, *reason)
})
t.Run("large page count", func(t *testing.T) {
pdf := NewPDF(strings.NewReader(pdfTwoPages))
pdf.maxPages = 1
reason := pdf.IsCorrupt(ctx)
assert.Equal(t, InvalidDocumentLargePageCount, *reason)
})
t.Run("version 1.0", func(t *testing.T) {
pdf := NewPDF(strings.NewReader(PDF1_0))
@@ -278,6 +268,62 @@ func TestIsCorrupt(t *testing.T) {
reason := pdf.IsCorrupt(ctx)
assert.Nil(t, reason)
})
t.Run("validate pdfs in assets/Generation", func(t *testing.T) {
var pdfFiles []string
dir := "../../../assets/sampleGeneration"
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.ToLower(filepath.Ext(path)) == ".pdf" {
pdfFiles = append(pdfFiles, path)
}
return nil
})
require.NoError(t, err)
for _, filename := range pdfFiles {
file, err := os.Open(filename)
require.NoError(t, err)
defer file.Close()
readSeeker := io.ReadSeeker(file)
pdf := NewPDF(readSeeker)
docErr := pdf.IsCorrupt(ctx)
assert.Nil(t, docErr)
}
})
t.Run("validate pdfs in assets/sampleOne", func(t *testing.T) {
var pdfFiles []string
dir := "../../../assets/sampleOne"
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.ToLower(filepath.Ext(path)) == ".pdf" {
pdfFiles = append(pdfFiles, path)
}
return nil
})
require.NoError(t, err)
for _, filename := range pdfFiles {
file, err := os.Open(filename)
require.NoError(t, err)
defer file.Close()
readSeeker := io.ReadSeeker(file)
pdf := NewPDF(readSeeker)
docErr := pdf.IsCorrupt(ctx)
assert.Nil(t, docErr)
}
})
}
func TestNewPDF(t *testing.T) {
@@ -285,8 +331,21 @@ func TestNewPDF(t *testing.T) {
pdf := NewPDF(strings.NewReader(pdfHelloWorld))
assert.NotNil(t, pdf.pdf)
assert.NotNil(t, pdf.config)
assert.Equal(t, TEXTRACT_PAGE_LIMIT, pdf.maxPages)
assert.Equal(t, TEXTRACT_SYNC_LIMIT, pdf.maxBytes)
assert.Equal(t, TEXTRACT_MIN_DIMENSION, pdf.minDimension)
assert.Equal(t, TEXTRACT_MAX_DIMENSION, pdf.maxDimension)
assert.Equal(t, TEXTRACT_MIN_DPI, pdf.minDPI)
assert.Equal(t, PNG_DEFAULT_DPI, pdf.pngDPI)
})
}
func TestPDFGetPage(t *testing.T) {
ctx := context.Background()
t.Run("hello world", func(t *testing.T) {
pdf := NewPDF(strings.NewReader(PDF1_7))
pageZero, err := pdf.GetPage(ctx, 0)
assert.NoError(t, err)
assert.Contains(t, string(pageZero), "PDF 1.7 Example")
})
}
@@ -413,51 +472,47 @@ startxref
%%EOF`
const PDF1_2 = `%PDF-1.2
%¥±ë
1 0 obj
<< /Type /Catalog
/Pages 2 0 R
>>
<</Type/Catalog/Pages 2 0 R>>
endobj
2 0 obj
<< /Type /Pages
/Kids [3 0 R]
/Count 1
>>
<</Type/Pages/Kids[3 0 R]/Count 1>>
endobj
3 0 obj
<< /Type /Page
/Parent 2 0 R
/MediaBox [0 0 612 792]
/Contents 4 0 R
>>
<</Type/Page/Parent 2 0 R/Resources<</Font<</F1 4 0 R>>>>/MediaBox[0 0 612 792]/Contents 5 0 R>>
endobj
4 0 obj
<< /Length 65 /Filter /FlateDecode >>
<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>
endobj
5 0 obj
<</Length 44>>
stream
xs
230W0 P&
(XY\R!$KHPS!$RRP )
BT
/F1 24 Tf
100 700 Td
(hello version 1.2) Tj
ET
endstream
endobj
xref
0 5
0 6
0000000000 65535 f
0000000009 00000 n
0000000018 00000 n
0000000063 00000 n
0000000125 00000 n
0000000210 00000 n
0000000114 00000 n
0000000223 00000 n
0000000285 00000 n
trailer
<< /Size 5
/Root 1 0 R
>>
<</Size 6/Root 1 0 R>>
startxref
325
377
%%EOF`
const PDF1_3 = `%PDF-1.3
+2 -2
View File
@@ -67,8 +67,8 @@ func TestSet(t *testing.T) {
textEntryId := uuid.New()
pool.ExpectQuery("name: GetTextEntryByDocId :one").WithArgs(params.DocumentID).
WillReturnRows(
pgxmock.NewRows([]string{"id", "documentId", "bucket", "key", "hash", "cleanId", "triggerId", "textractJobId", "triggerVersion", "extractionVersion"}).
AddRow(textEntryId, params.DocumentID, "buket", "/i/am/here", "example", uuid.New(), uuid.New(), "jobId", int64(123), int64(543)),
pgxmock.NewRows([]string{"id", "documentId", "bucket", "key", "hash", "cleanId", "extractionVersion"}).
AddRow(textEntryId, params.DocumentID, "buket", "/i/am/here", "example", uuid.New(), int64(543)),
)
pool.ExpectQuery("name: GetQuery :one").WithArgs(query.ID).WillReturnRows(
pgxmock.NewRows([]string{"id", "type", "activeVersion", "latestVersion", "config", "requiredIds"}).
+3 -2
View File
@@ -26,8 +26,9 @@ func TestNew(t *testing.T) {
cfg := &BaseConfig{}
test.SetCfgProvider(t, cfg)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{})
defer cleanup()
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{})
test.SetDBCfg(t, cfg)
registerHandlers := func() (*openapi3.T, error) {
return &openapi3.T{}, nil
+4 -3
View File
@@ -28,13 +28,14 @@ func TestNew(t *testing.T) {
ctx := context.Background()
cfg := &TestConfig{}
test.SetCfgProvider(t, cfg)
net := test.DepNetwork.Get(t, ctx)
_, acleanup := test.CreateAWSContainer(t, ctx, cfg, &test.CreateAWSConfig{})
_, acleanup := test.CreateAWSContainer(t, ctx, cfg, net)
defer acleanup()
err := cfg.SetQueueClient(ctx)
require.NoError(t, err)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{})
defer cleanup()
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{})
test.SetDBCfg(t, cfg)
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
+3 -2
View File
@@ -21,9 +21,10 @@ func TestNew(t *testing.T) {
cfg := &server.BaseConfig{}
test.SetCfgProvider(t, cfg)
net := test.DepNetwork.Get(t, ctx)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{})
defer cleanup()
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{})
test.SetDBCfg(t, cfg)
clean, err := server.New(ctx, cfg)
require.NoError(t, err)
@@ -25,6 +25,7 @@ type DBConfig struct {
type ConfigProvider interface {
GetDBBaseURI() string
GetDBURI() string
GetDBAdminDBURI() string
GetDBOpts() map[string]string
GetDBOptsString() string
GetDBHost() string
@@ -82,6 +83,10 @@ func (b *DBConfig) GetDBBaseURI() string {
return fmt.Sprintf("%s://%s:%s@%s:%d/", b.GetDBDriver(), b.DBUser, b.DBSecret, b.DBHost, b.DBPort)
}
func (b *DBConfig) GetDBAdminDBURI() string {
return fmt.Sprintf("%s%s?%s", b.GetDBBaseURI(), b.GetDBDriver(), b.GetDBOptsString())
}
func (b *DBConfig) GetDBURI() string {
return fmt.Sprintf("%s%s?%s", b.GetDBBaseURI(), b.DBName, b.GetDBOptsString())
}
@@ -67,6 +67,22 @@ func TestGetDBURI(t *testing.T) {
assert.Equal(t, "postgres://user:pass@host:123/name?sslmode=disable", uri)
}
func TestGetDBAdminDBURI(t *testing.T) {
cfg := database.DBConfig{}
uri := cfg.GetDBURI()
assert.Equal(t, "postgres://:@:0/?", uri)
cfg.DBHost = "host"
cfg.DBPort = 123
cfg.DBUser = "user"
cfg.DBSecret = "pass"
cfg.DBName = "name"
cfg.DBNoSSL = true
uri = cfg.GetDBAdminDBURI()
assert.Equal(t, "postgres://user:pass@host:123/postgres?sslmode=disable", uri)
}
func TestGetDBHost(t *testing.T) {
cfg := database.DBConfig{}
+2 -2
View File
@@ -22,10 +22,10 @@ func TestSetDBPool(t *testing.T) {
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, cleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{
RunMigrations: true,
})
defer cleanup()
err := cfg.SetDBPool(ctx)
require.NoError(t, err)
@@ -24,10 +24,9 @@ type BucketKey struct {
}
const (
Import BucketLocation = "import"
TextTextract BucketLocation = "text/textract"
TextOut BucketLocation = "text/out"
Export BucketLocation = "export"
Import BucketLocation = "import"
Text BucketLocation = "text"
Export BucketLocation = "export"
)
var PartExclusions = []BucketLocation{
@@ -133,7 +132,7 @@ func (c *BucketKey) parseClientID(name string) error {
}
func (c *BucketKey) parseLocation(name string) error {
c.Location = BucketLocation(strings.ReplaceAll(name, "-", "/"))
pattern := fmt.Sprintf(`^(%s|%s|%s|%s)$`, Import, TextTextract, TextOut, Export)
pattern := fmt.Sprintf(`^(%s|%s|%s)$`, Import, Text, Export)
reg := regexp.MustCompile(pattern)
matches := reg.FindStringSubmatch(string(c.Location))
if len(matches) != 2 {
@@ -19,20 +19,16 @@ func TestGetBucketKey(t *testing.T) {
assert.Equal(t, "AAA//20250331/0/2025-03-31T040816Z_AAA__b745910f-f529-43c5-87e1-25629d46ef40", key.String())
key.Location = Import
assert.Equal(t, "AAA/import/20250331/0/2025-03-31T040816Z_AAA_import_b745910f-f529-43c5-87e1-25629d46ef40", key.String())
key.Location = TextTextract
assert.Equal(t, "AAA/text/textract/20250331/0/2025-03-31T040816Z_AAA_text-textract_b745910f-f529-43c5-87e1-25629d46ef40", key.String())
key.Location = TextOut
assert.Equal(t, "AAA/text/out/20250331/0/2025-03-31T040816Z_AAA_text-out_b745910f-f529-43c5-87e1-25629d46ef40", key.String())
key.Location = Text
assert.Equal(t, "AAA/text/20250331/0/2025-03-31T040816Z_AAA_text_b745910f-f529-43c5-87e1-25629d46ef40", key.String())
key.Location = Export
assert.Equal(t, "AAA/export/20250331/2025-03-31T040816Z_AAA_export_b745910f-f529-43c5-87e1-25629d46ef40", key.String())
part := uint16(3)
key.Part = &part
key.Location = Import
assert.Equal(t, "AAA/import/20250331/3/2025-03-31T040816Z_AAA_import_b745910f-f529-43c5-87e1-25629d46ef40", key.String())
key.Location = TextTextract
assert.Equal(t, "AAA/text/textract/20250331/3/2025-03-31T040816Z_AAA_text-textract_b745910f-f529-43c5-87e1-25629d46ef40", key.String())
key.Location = TextOut
assert.Equal(t, "AAA/text/out/20250331/3/2025-03-31T040816Z_AAA_text-out_b745910f-f529-43c5-87e1-25629d46ef40", key.String())
key.Location = Text
assert.Equal(t, "AAA/text/20250331/3/2025-03-31T040816Z_AAA_text_b745910f-f529-43c5-87e1-25629d46ef40", key.String())
key.Location = Export
assert.Equal(t, "AAA/export/20250331/2025-03-31T040816Z_AAA_export_b745910f-f529-43c5-87e1-25629d46ef40", key.String())
filetype := "csv"
@@ -67,24 +63,15 @@ func TestParseBucketKey(t *testing.T) {
CreatedAt: time.Date(2025, 3, 31, 0, 0, 0, 0, time.UTC),
}, key)
key, err = ParseBucketKey("AAA/text/textract/20250331/2025-03-31T040816Z_AAA_text-textract_b745910f-f529-43c5-87e1-25629d46ef40")
key, err = ParseBucketKey("AAA/text/20250331/2025-03-31T040816Z_AAA_text_b745910f-f529-43c5-87e1-25629d46ef40")
assert.NoError(t, err)
assert.EqualExportedValues(t, BucketKey{
ClientID: "AAA",
Location: TextTextract,
Location: Text,
EntityID: uuid.MustParse("b745910f-f529-43c5-87e1-25629d46ef40"),
CreatedAt: time.Date(2025, 2, 31, 0, 0, 0, 0, time.UTC),
}, key)
key, err = ParseBucketKey("AAA/text/out/20250331/2025-03-31T040816Z_AAA_text-out_b745910f-f529-43c5-87e1-25629d46ef40")
assert.NoError(t, err)
assert.EqualExportedValues(t, BucketKey{
ClientID: "AAA",
Location: TextOut,
EntityID: uuid.MustParse("b745910f-f529-43c5-87e1-25629d46ef40"),
CreatedAt: time.Date(2025, 3, 31, 0, 0, 0, 0, time.UTC),
}, key)
key, err = ParseBucketKey("AAA/export/20250331/2025-03-31T040816Z_AAA_export_b745910f-f529-43c5-87e1-25629d46ef40")
assert.NoError(t, err)
assert.EqualExportedValues(t, BucketKey{
@@ -105,21 +92,11 @@ func TestParseBucketKey(t *testing.T) {
Part: &part,
}, key)
key, err = ParseBucketKey("AAA/text/textract/20250331/3/2025-03-31T040816Z_AAA_text-textract_b745910f-f529-43c5-87e1-25629d46ef40")
key, err = ParseBucketKey("AAA/text/20250331/3/2025-03-31T040816Z_AAA_text_b745910f-f529-43c5-87e1-25629d46ef40")
assert.NoError(t, err)
assert.EqualExportedValues(t, BucketKey{
ClientID: "AAA",
Location: TextTextract,
EntityID: uuid.MustParse("b745910f-f529-43c5-87e1-25629d46ef40"),
CreatedAt: time.Date(2025, 3, 31, 0, 0, 0, 0, time.UTC),
Part: &part,
}, key)
key, err = ParseBucketKey("AAA/text/out/20250331/3/2025-03-31T040816Z_AAA_text-out_b745910f-f529-43c5-87e1-25629d46ef40")
assert.NoError(t, err)
assert.EqualExportedValues(t, BucketKey{
ClientID: "AAA",
Location: TextOut,
Location: Text,
EntityID: uuid.MustParse("b745910f-f529-43c5-87e1-25629d46ef40"),
CreatedAt: time.Date(2025, 3, 31, 0, 0, 0, 0, time.UTC),
Part: &part,
@@ -94,7 +94,8 @@ func TestCalculateETag(t *testing.T) {
cfg := &StoreConfig{}
test.SetCfgProvider(t, cfg)
acfg, clean := test.CreateAWSContainer(t, ctx, cfg, &test.CreateAWSConfig{})
net := test.DepNetwork.Get(t, ctx)
acfg, clean := test.CreateAWSContainer(t, ctx, cfg, net)
defer clean()
test.SetStoreClient(t, ctx, cfg, acfg.ExternalEndpoint)
test.CreateBucket(t, ctx, cfg, test.BucketName)
@@ -1,15 +0,0 @@
package documenttextprocess
const EnvName = "DOCUMENT_TEXT_PROCESS_URL"
type DocTextProcessConfig struct {
DocumentTextProcessURL string `env:"DOCUMENT_TEXT_PROCESS_URL,required,notEmpty"`
}
func (c *DocTextProcessConfig) GetDocumentTextProcessURL() string {
return c.DocumentTextProcessURL
}
type ConfigProvider interface {
GetDocumentTextProcessURL() string
}
@@ -1,21 +0,0 @@
package documenttextprocess_test
import (
"testing"
"queryorchestration/internal/serviceconfig/queue/documenttextprocess"
"github.com/stretchr/testify/assert"
)
func TestGetDocumentTextURL(t *testing.T) {
cfg := documenttextprocess.DocTextProcessConfig{}
name := cfg.GetDocumentTextProcessURL()
assert.Equal(t, "", name)
cfg.DocumentTextProcessURL = "name"
name = cfg.GetDocumentTextProcessURL()
assert.Equal(t, "name", name)
assert.Equal(t, cfg.DocumentTextProcessURL, name)
}
+2 -5
View File
@@ -10,7 +10,6 @@ import (
"github.com/docker/go-connections/nat"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
)
type APIName string
@@ -26,19 +25,17 @@ type API struct {
type APIConfig struct {
API API
Network *testcontainers.DockerNetwork
MockHTTP string
}
func CreateAPI(t testing.TB, ctx context.Context, cfg serviceconfig.ConfigProvider, config *APIConfig) (*Container, func()) {
func CreateAPI(t testing.TB, ctx context.Context, cfg serviceconfig.ConfigProvider, network string, config *APIConfig) (*Container, func()) {
port, err := nat.NewPort("tcp", "8080")
require.NoError(t, err)
container, cleanup := createContainer(t, ctx, &containerConfig{
container, cleanup := createContainer(t, ctx, network, &containerConfig{
Cfg: cfg,
Name: string(config.API.Name),
DownstreamQueues: config.API.DownstreamQueues,
Network: config.Network,
MockHTTP: config.MockHTTP,
WaitForMsg: "⇨ http server started on [::]:8080",
})
+5 -12
View File
@@ -1,7 +1,6 @@
package test_test
import (
"context"
"testing"
"queryorchestration/internal/serviceconfig"
@@ -14,24 +13,18 @@ func TestCreateAPI(t *testing.T) {
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
ctx := context.Background()
ncfg, ncleanup := test.CreateNetwork(t, ctx)
defer ncleanup()
ctx := t.Context()
cfg := &serviceconfig.BaseConfig{}
test.SetCfgProvider(t, cfg)
_, dbcleanup := test.CreateDB(t, ctx, cfg, &test.CreateDatabaseConfig{
Network: ncfg,
})
defer dbcleanup()
net := test.DepNetwork.Get(t, ctx)
_ = test.CreateDB(t, ctx, cfg, net, &test.CreateDatabaseConfig{})
acfg := &test.APIConfig{
API: test.QueryAPI,
Network: ncfg,
API: test.QueryAPI,
}
conn, cleanup := test.CreateAPI(t, ctx, cfg, acfg)
conn, cleanup := test.CreateAPI(t, ctx, cfg, net, acfg)
assert.NotNil(t, conn)
assert.NotNil(t, cleanup)
+36
View File
@@ -0,0 +1,36 @@
package test
import (
"encoding/json"
"io"
"os"
"testing"
"github.com/aws/aws-sdk-go-v2/service/textract"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func GetTextractFileResponse(t testing.TB, filename string) map[string][]textract.DetectDocumentTextOutput {
file, err := os.Open(filename)
require.NoError(t, err)
defer file.Close()
decoder := json.NewDecoder(file)
var out map[string][]textract.DetectDocumentTextOutput
err = decoder.Decode(&out)
require.NoError(t, err)
return out
}
func AssertStringToReader(t testing.TB, expected string, actual io.Reader) {
actualBytes, err := io.ReadAll(actual)
require.NoError(t, err)
actualStr := string(actualBytes)
if !assert.Equal(t, expected, actualStr) {
diff := cmp.Diff(expected, actualStr)
t.Logf("Detailed diff (-expected +actual):\n%s", diff)
}
}
+29
View File
@@ -0,0 +1,29 @@
package test_test
import (
"strings"
"testing"
"queryorchestration/internal/test"
"github.com/aws/aws-sdk-go-v2/service/textract"
"github.com/stretchr/testify/assert"
)
func TestAssertStringToReader(t *testing.T) {
t.Run("pass", func(t *testing.T) {
mockT := &testing.T{}
test.AssertStringToReader(t, "hi", strings.NewReader("hi"))
assert.False(t, mockT.Failed())
})
t.Run("fail", func(t *testing.T) {
mockT := &testing.T{}
test.AssertStringToReader(mockT, "bye", strings.NewReader("hi"))
assert.True(t, mockT.Failed())
})
}
func TestGetTextractFileResponse(t *testing.T) {
out := test.GetTextractFileResponse(t, "../../assets/sampleGeneration/generated/helloWorld.gen")
assert.IsType(t, map[string][]textract.DetectDocumentTextOutput{}, out)
}
+16 -27
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net/http"
"strconv"
"testing"
"time"
@@ -20,11 +21,6 @@ import (
type AWSContainerConfig struct {
Container testcontainers.Container
ExternalEndpoint string
NetworkEndpoint string
}
type CreateAWSConfig struct {
Network *testcontainers.DockerNetwork
}
type AWSConfigProvider interface {
@@ -32,10 +28,15 @@ type AWSConfigProvider interface {
objectstore.ConfigProvider
}
func CreateAWSContainer(t testing.TB, ctx context.Context, cfg AWSConfigProvider, acfg *CreateAWSConfig) (*AWSContainerConfig, func()) {
alias := "localstack"
const (
awsAlias = "localstack"
awsPort = 4566
)
port, err := nat.NewPort("tcp", "4566")
func CreateAWSContainer(t testing.TB, ctx context.Context, cfg AWSConfigProvider, network string) (*AWSContainerConfig, func()) {
alias := NormaliseAlias(fmt.Sprintf("%s_%s", awsAlias, t.Name()))
port, err := nat.NewPort("tcp", strconv.FormatInt(int64(awsPort), 10))
require.NoError(t, err)
req := testcontainers.ContainerRequest{
@@ -69,13 +70,10 @@ func CreateAWSContainer(t testing.TB, ctx context.Context, cfg AWSConfigProvider
return statusCode == http.StatusOK
}),
),
}
if acfg.Network != nil {
req.Networks = []string{acfg.Network.Name}
req.NetworkAliases = map[string][]string{
acfg.Network.Name: {alias},
}
Networks: []string{network},
NetworkAliases: map[string][]string{
network: {alias},
},
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
@@ -90,25 +88,16 @@ func CreateAWSContainer(t testing.TB, ctx context.Context, cfg AWSConfigProvider
require.NoError(t, err)
extEndpoint := fmt.Sprintf("http://%s:%s", host, mappedPort.Port())
t.Setenv("AWS_ENDPOINT_URL", extEndpoint)
cfg.SetAWSEndpoint(extEndpoint)
t.Setenv("AWS_ENDPOINT_URL_SQS", extEndpoint)
cfg.SetSQSEndpoint(extEndpoint)
t.Setenv("AWS_ENDPOINT_URL_S3", extEndpoint)
cfg.SetS3Endpoint(extEndpoint)
var endpoint string
if acfg.Network != nil {
endpoint = fmt.Sprintf("http://%s:%s", alias, port.Port())
cfg.SetAWSEndpoint(endpoint)
cfg.SetSQSEndpoint(endpoint)
cfg.SetS3Endpoint(endpoint)
}
t.Setenv("AWS_ENDPOINT_URL", extEndpoint)
t.Setenv("AWS_ENDPOINT_URL_SQS", extEndpoint)
t.Setenv("AWS_ENDPOINT_URL_S3", extEndpoint)
return &AWSContainerConfig{
Container: container,
ExternalEndpoint: extEndpoint,
NetworkEndpoint: endpoint,
}, func() {
err := container.Terminate(ctx)
if err != nil {

Some files were not shown because too many files have changed in this diff Show More