Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,32 @@ require (
)

require (
baliance.com/gooxml v1.0.1 // indirect
github.com/jung-kurt/gofpdf v1.16.2 // indirect
)

require (
github.com/Achiket123/html2docx v0.1.2
github.com/Yamashou/gqlgenc v0.33.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.9 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.9 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
Expand Down
54 changes: 54 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
baliance.com/gooxml v1.0.1 h1:fG5lmxmjEVFfbKQ2NuyCuU3hMuuOb5avh5a38SZNO1o=
baliance.com/gooxml v1.0.1/go.mod h1:+gpUgmkAF4zCtwOFPNRLDAvpVRWoKs5EeQTSv/HYFnw=
github.com/99designs/gqlgen v0.17.86 h1:C8N3UTa5heXX6twl+b0AJyGkTwYL6dNmFrgZNLRcU6w=
github.com/99designs/gqlgen v0.17.86/go.mod h1:KTrPl+vHA1IUzNlh4EYkl7+tcErL3MgKnhHrBcV74Fw=
github.com/Achiket123/html2docx v0.1.1 h1:QgmOpyMrNolPHRzxoEi7IyYM8w9Krm5Y4whk4uO3jt0=
github.com/Achiket123/html2docx v0.1.1/go.mod h1:uPSyBD8pW6kzLhIyDJevKaVVe+Q2mg9u2jlrAdxW5g0=
github.com/Achiket123/html2docx v0.1.2 h1:BxvcoR2jlLHbMgyUkQI/h0JzB/vkWnBdPQfaa3Y5uPY=
github.com/Achiket123/html2docx v0.1.2/go.mod h1:uPSyBD8pW6kzLhIyDJevKaVVe+Q2mg9u2jlrAdxW5g0=
github.com/99designs/gqlgen v0.17.89 h1:KzEcxPiMgQoMw3m/E85atUEHyZyt0PbAflMia5Kw8z8=
github.com/99designs/gqlgen v0.17.89/go.mod h1:GFqruTVGB7ZTdrf1uzOagpXbY7DrEt1pIxnTdhIbWvQ=
github.com/Yamashou/gqlgenc v0.33.0 h1:0fxTnNE8/JVmFpfo7reA5pEgOcr7VjNc+/nEpVhNjfc=
Expand All @@ -8,12 +16,55 @@ github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7O
github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=
github.com/aws/aws-sdk-go-v2/config v1.32.9/go.mod h1:U+fCQ+9QKsLW786BCfEjYRj34VVTbPdsLP3CHSYXMOI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw=
github.com/aws/aws-sdk-go-v2/credentials v1.19.9/go.mod h1:+J44MBhmfVY/lETFiKI+klz0Vym2aCmIjqgClMmW82w=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
Expand Down Expand Up @@ -155,6 +206,8 @@ github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNs
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pdfcpu/pdfcpu v0.11.1 h1:htHBSkGH5jMKWC6e0sihBFbcKZ8vG1M67c8/dJxhjas=
github.com/pdfcpu/pdfcpu v0.11.1/go.mod h1:pP3aGga7pRvwFWAm9WwFvo+V68DfANi9kxSQYioNYcw=
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down Expand Up @@ -213,6 +266,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
Expand Down
234 changes: 223 additions & 11 deletions pkg/jobs/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
"errors"
"fmt"
"maps"
"os"
"strings"
"time"
"path/filepath"

"github.com/99designs/gqlgen/graphql"
"github.com/gertd/go-pluralize"
Expand All @@ -24,9 +26,8 @@
"github.com/theopenlane/httpsling"
"github.com/theopenlane/iam/auth"

html2docx "github.com/Achiket123/html2docx/converter"
"github.com/theopenlane/riverboat/pkg/jobs/openlane"

goclient "github.com/theopenlane/go-client"
)

var defaultPageSize int64 = 100
Expand Down Expand Up @@ -157,6 +158,22 @@

fields := export.Export.Fields

// Ensure `details` is in the fields list so we can extract it for document generation.
// but we need it for DOCX, PDF, and MD exports.
// So just for csv we are not adding it
if export.Export.Format != enums.ExportFormatCsv {
hasDetails := false
for _, f := range fields {
if f == "details" {
hasDetails = true
break
}
}
if !hasDetails {
fields = append(fields, "details")
}
}

query := w.buildGraphQLQuery(rootQuery, exportType, fields, hasWhere)

var (
Expand Down Expand Up @@ -184,27 +201,79 @@
return w.updateExportStatus(ctx, job.Args.ExportID, enums.ExportStatusNodata, nil)
}

csvData, err := w.marshalToCSV(allNodes)
if err != nil {
log.Error().Err(err).Msg("failed to marshal to CSV")
return w.updateExportStatus(ctx, job.Args.ExportID, enums.ExportStatusFailed, err)
// Determine the output format from the export record
exportFormat := export.Export.Format
timestamp := time.Now().Format("20060102_150405")

var (
fileData []byte
filename string
contentType string
)

switch exportFormat {
case enums.ExportFormatDocx:
htmlStrings := extractDetailsStrings(allNodes)

fileData, err = marshalToDocx(htmlStrings)
if err != nil {
log.Error().Err(err).Msg("failed to marshal to DOCX")
return w.updateExportStatus(ctx, job.Args.ExportID, enums.ExportStatusFailed, err)
}

filename = fmt.Sprintf("%s_export_%s_%s.docx", rootQuery, job.Args.ExportID, timestamp)
contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"

case enums.ExportFormatPdf:
htmlStrings := extractDetailsStrings(allNodes)

fileData, err = marshalToPDF(htmlStrings)
if err != nil {
log.Error().Err(err).Msg("failed to marshal to PDF")
return w.updateExportStatus(ctx, job.Args.ExportID, enums.ExportStatusFailed, err)
}

filename = fmt.Sprintf("%s_export_%s_%s.pdf", rootQuery, job.Args.ExportID, timestamp)
contentType = "application/pdf"

case enums.ExportFormatMarkDown:
htmlStrings := extractDetailsStrings(allNodes)

fileData, err = marshalToMarkdown(htmlStrings)
if err != nil {
log.Error().Err(err).Msg("failed to marshal to Markdown")
return w.updateExportStatus(ctx, job.Args.ExportID, enums.ExportStatusFailed, err)
}

filename = fmt.Sprintf("%s_export_%s_%s.md", rootQuery, job.Args.ExportID, timestamp)
contentType = "text/markdown"

default:
// Default to CSV (includes ExportFormatCsv and any unknown format)
fileData, err = w.marshalToCSV(allNodes)
if err != nil {
log.Error().Err(err).Msg("failed to marshal to CSV")
return w.updateExportStatus(ctx, job.Args.ExportID, enums.ExportStatusFailed, err)
}

filename = fmt.Sprintf("%s_export_%s_%s.csv", rootQuery, job.Args.ExportID, timestamp)
contentType = "text/csv"
}

filename := fmt.Sprintf("%s_export_%s_%s.csv", rootQuery, job.Args.ExportID, time.Now().Format("20060102_150405"))
reader := bytes.NewReader(csvData)
reader := bytes.NewReader(fileData)

upload := &graphql.Upload{
File: reader,
Filename: filename,
Size: int64(len(csvData)),
ContentType: "text/csv",
Size: int64(len(fileData)),
ContentType: contentType,
}

updateInput := graphclient.UpdateExportInput{
Status: &enums.ExportStatusReady,
}

_, err = w.olClient.UpdateExport(ctx, job.Args.ExportID, updateInput, []*graphql.Upload{upload}, goclient.WithImpersonationInterceptor(job.Args.UserID, job.Args.OrganizationID))
_, err = w.olClient.UpdateExport(ctx, job.Args.ExportID, updateInput, []*graphql.Upload{upload})
if err != nil {
log.Error().Err(err).Msg("failed to update export with file")
return w.updateExportStatus(ctx, job.Args.ExportID, enums.ExportStatusFailed, err)
Expand Down Expand Up @@ -594,3 +663,146 @@

return cleaned
}

// extractDetailsStrings extracts string content from nodes and formats them into
// HTML strings for PDF/DOCX/MD generation. It includes ID, CreatedAt, Name, Status,
// and the Details (or fallback content).
func extractDetailsStrings(nodes []map[string]any) []string {

Check failure on line 670 in pkg/jobs/export.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 41 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=theopenlane_riverboat&issues=AZ1d4LLGrHfHDGLi-XrO&open=AZ1d4LLGrHfHDGLi-XrO&pullRequest=236
if len(nodes) == 0 {
return nil
}

var results []string

for _, n := range nodes {
flat := make(map[string]any)
flatten("", n, flat)

var buf strings.Builder

// Add common headers if they exist
if id, ok := flat["id"]; ok && id != nil {
buf.WriteString(fmt.Sprintf("<p><strong>ID:</strong> %v</p>\n", id))
}
Comment on lines +683 to +686
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would leave off id

Suggested change
// Add common headers if they exist
if id, ok := flat["id"]; ok && id != nil {
buf.WriteString(fmt.Sprintf("<p><strong>ID:</strong> %v</p>\n", id))
}
// Add common headers if they exist

if createdAt, ok := flat["created_at"]; ok && createdAt != nil {
buf.WriteString(fmt.Sprintf("<p><strong>Created At:</strong> %v</p>\n", createdAt))
}
if name, ok := flat["name"]; ok && name != nil {
buf.WriteString(fmt.Sprintf("<p><strong>Name:</strong> %v</p>\n", name))
}
if status, ok := flat["status"]; ok && status != nil {
buf.WriteString(fmt.Sprintf("<p><strong>Status:</strong> %v</p>\n", status))
}
Comment on lines +687 to +695
Copy link
Copy Markdown
Member

@golanglemonade golanglemonade Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use these:

Suggested change
if createdAt, ok := flat["created_at"]; ok && createdAt != nil {
buf.WriteString(fmt.Sprintf("<p><strong>Created At:</strong> %v</p>\n", createdAt))
}
if name, ok := flat["name"]; ok && name != nil {
buf.WriteString(fmt.Sprintf("<p><strong>Name:</strong> %v</p>\n", name))
}
if status, ok := flat["status"]; ok && status != nil {
buf.WriteString(fmt.Sprintf("<p><strong>Status:</strong> %v</p>\n", status))
}
if name, ok := flat["name"]; ok && name != nil {
buf.WriteString(fmt.Sprintf("<p><strong>Name:</strong> %v</p>\n", name))
}
if status, ok := flat["status"]; ok && status != nil {
buf.WriteString(fmt.Sprintf("<p><strong>Status:</strong> %v</p>\n", status))
}
if updatedAt, ok := flat["updated_at"]; ok && updatedAt != nil {
buf.WriteString(fmt.Sprintf("<p><strong>Last Updated At:</strong> %v</p>\n", createdAt))
}
if updatedBy, ok := flat["updated_by"]; ok && updatedBy != nil {
buf.WriteString(fmt.Sprintf("<p><strong>LastUpdatedBy By:</strong> %v</p>\n", createdAt))
}
if revision, ok := flat["revision"]; ok && revision != nil {
buf.WriteString(fmt.Sprintf("<p><strong>Version:</strong> %v</p>\n", createdAt))
}


buf.WriteString("<hr/>\n")

// Add the main content (details or fallback)
if details, ok := flat["details"]; ok && details != nil {
str := fmt.Sprint(details)
log.Info().Str("raw_details", str).Msg("extracted details field for debugging slate.js format")
if !strings.Contains(str, "<p>") && !strings.Contains(str, "<div>") && !strings.Contains(str, "<br") {
str = strings.ReplaceAll(str, "\n", "<br/>\n")
}
buf.WriteString(fmt.Sprintf("<div><strong>Summary:</strong><br/>\n%s</div>\n", str))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for a header here

Suggested change
buf.WriteString(fmt.Sprintf("<div><strong>Summary:</strong><br/>\n%s</div>\n", str))
buf.WriteString(fmt.Sprintf("<div>%s</div>\n", str))

} else {
// Fallback: extract string representation for any val
buf.WriteString("<div><strong>Summary:</strong><br/>\n")
for k, v := range flat {
// skip the headers we already added
if k == "id" || k == "created_at" || k == "name" || k == "status" {
continue
}
if v != nil {
str := fmt.Sprint(v)
if strings.TrimSpace(str) != "" {
buf.WriteString(fmt.Sprintf("<strong>%s:</strong> %s<br/>\n", k, str))
}
}
}
buf.WriteString("</div>\n")
}

if buf.Len() > 0 {
results = append(results, buf.String())
}
}
Comment on lines +707 to +728
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need an else. if details is empty, there is not content we want to be adding.

Suggested change
} else {
// Fallback: extract string representation for any val
buf.WriteString("<div><strong>Summary:</strong><br/>\n")
for k, v := range flat {
// skip the headers we already added
if k == "id" || k == "created_at" || k == "name" || k == "status" {
continue
}
if v != nil {
str := fmt.Sprint(v)
if strings.TrimSpace(str) != "" {
buf.WriteString(fmt.Sprintf("<strong>%s:</strong> %s<br/>\n", k, str))
}
}
}
buf.WriteString("</div>\n")
}
if buf.Len() > 0 {
results = append(results, buf.String())
}
}
}
}

return results
}

// marshalToDocx converts HTML content to DOCX format bytes using the html2docx converter.
func marshalToDocx(htmlContents []string) ([]byte, error) {
conv := html2docx.NewHTMLToDocxConverter()

if err := conv.Convert(htmlContents); err != nil {
return nil, fmt.Errorf("docx conversion failed: %w", err)
}

tmpPath, cleanup, err := createTempFile(".docx")
if err != nil {
return nil, err
}
defer cleanup()

if err := conv.SaveToFile(tmpPath); err != nil {
return nil, fmt.Errorf("failed to save docx: %w", err)
}

// #nosec G304 -- safe: path generated via MkdirTemp, not user input
data, err := os.ReadFile(tmpPath)
if err != nil {
return nil, fmt.Errorf("failed to read docx file: %w", err)
}

return data, nil
}
// marshalToPDF converts HTML content to PDF format bytes using the html2docx converter.
func marshalToPDF(htmlContents []string) ([]byte, error) {
conv := html2docx.NewHTMLToPDFConverter()

if err := conv.Convert(htmlContents); err != nil {
return nil, fmt.Errorf("pdf conversion failed: %w", err)
}

tmpPath, cleanup, err := createTempFile(".pdf")
if err != nil {
return nil, err
}
defer cleanup()

if err := conv.SaveToFile(tmpPath); err != nil {
return nil, fmt.Errorf("failed to save pdf: %w", err)
}

// #nosec G304 -- safe: path generated internally
data, err := os.ReadFile(tmpPath)
if err != nil {
return nil, fmt.Errorf("failed to read pdf file: %w", err)
}

return data, nil
}
// marshalToMarkdown converts HTML content to Markdown format bytes using the html2docx converter.
func marshalToMarkdown(htmlContents []string) ([]byte, error) {
conv := html2docx.NewHTMLToMarkdownConverter()

md, err := conv.Convert(htmlContents)
if err != nil {
return nil, fmt.Errorf("markdown conversion failed: %w", err)
}

return []byte(md), nil
}

func createTempFile(ext string) (string, func(), error) {
tmpDir, err := os.MkdirTemp("", "export-*")
if err != nil {
return "", nil, fmt.Errorf("failed to create temp dir: %w", err)
}

cleanup := func() {
_ = os.RemoveAll(tmpDir)
}

tmpPath := filepath.Join(tmpDir, "file"+ext)
return tmpPath, cleanup, nil
}