Initial export

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-09-04 10:34:21 +02:00
committed by Sacha Al Himdani
parent a11dfdb244
commit d47cac911d
13 changed files with 1022 additions and 63 deletions

View File

@@ -49,4 +49,5 @@ const (
SnapshotEntityType
ContinualImprovementRegistryEntityType
ProcessingActivityRegistryEntityType
FrameworkExportEntityType
)

View File

@@ -0,0 +1,163 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"context"
"errors"
"fmt"
"maps"
"time"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/page"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
)
type (
FrameworkExport struct {
ID gid.GID `db:"id"`
FrameworkID gid.GID `db:"framework_id"`
Status FrameworkExportStatus `db:"status"`
FileID *gid.GID `db:"file_id"`
CreatedAt time.Time `db:"created_at"`
StartedAt *time.Time `db:"started_at"`
CompletedAt *time.Time `db:"completed_at"`
}
FrameworkExports []*FrameworkExport
)
var (
ErrNoFrameworkExportAvailable = errors.New("no framework export available")
)
func (fe FrameworkExport) CursorKey(orderBy FrameworkExportOrderField) page.CursorKey {
switch orderBy {
case FrameworkExportOrderFieldCreatedAt:
return page.NewCursorKey(fe.ID, fe.CreatedAt)
}
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
}
func (fe *FrameworkExport) Insert(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
INSERT INTO framework_exports (
id,
tenant_id,
framework_id,
status,
created_at,
expires_at
) VALUES (
@id,
@tenant_id,
@framework_id,
@status,
@created_at,
@expires_at
)`
args := pgx.StrictNamedArgs{
"id": fe.ID,
"tenant_id": scope.GetTenantID(),
"framework_id": fe.FrameworkID,
"status": fe.Status,
"created_at": fe.CreatedAt,
}
_, err := conn.Exec(ctx, q, args)
return err
}
func (fe *FrameworkExport) Update(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
UPDATE
framework_exports
SET
status = @status,
file_id = @file_id,
started_at = @started_at,
completed_at = @completed_at
WHERE
%s
AND id = @id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"status": fe.Status,
"file_id": fe.FileID,
"started_at": fe.StartedAt,
"completed_at": fe.CompletedAt,
"id": fe.ID,
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args)
return err
}
func (fe *FrameworkExport) LoadNextPendingForUpdateSkipLocked(
ctx context.Context,
conn pg.Conn,
) error {
q := `
SELECT
id,
framework_id,
status,
created_at,
started_at,
completed_at
FROM
framework_exports
WHERE
status = @status
ORDER BY
created_at ASC
LIMIT 1
FOR UPDATE SKIP LOCKED
`
args := pgx.StrictNamedArgs{"status": FrameworkExportStatusPending}
rows, err := conn.Query(ctx, q, args)
if err != nil {
return err
}
fe2, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FrameworkExport])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ErrNoFrameworkExportAvailable
}
return fmt.Errorf("cannot collect framework export: %w", err)
}
*fe = fe2
return nil
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
type (
FrameworkExportOrderField string
)
const (
FrameworkExportOrderFieldCreatedAt FrameworkExportOrderField = "CREATED_AT"
)
func (p FrameworkExportOrderField) Column() string {
return string(p)
}
func (p FrameworkExportOrderField) String() string {
return string(p)
}
func (p FrameworkExportOrderField) MarshalText() ([]byte, error) {
return []byte(p.String()), nil
}
func (p *FrameworkExportOrderField) UnmarshalText(text []byte) error {
*p = FrameworkExportOrderField(text)
return nil
}

View File

@@ -0,0 +1,86 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"database/sql/driver"
"fmt"
)
type (
FrameworkExportStatus string
)
const (
FrameworkExportStatusPending FrameworkExportStatus = "pending"
FrameworkExportStatusProcessing FrameworkExportStatus = "processing"
FrameworkExportStatusCompleted FrameworkExportStatus = "completed"
FrameworkExportStatusFailed FrameworkExportStatus = "failed"
)
func (pvs FrameworkExportStatus) MarshalText() ([]byte, error) {
return []byte(pvs.String()), nil
}
func (pvs *FrameworkExportStatus) UnmarshalText(data []byte) error {
val := string(data)
switch val {
case FrameworkExportStatusPending.String():
*pvs = FrameworkExportStatusPending
case FrameworkExportStatusProcessing.String():
*pvs = FrameworkExportStatusProcessing
case FrameworkExportStatusCompleted.String():
*pvs = FrameworkExportStatusCompleted
case FrameworkExportStatusFailed.String():
*pvs = FrameworkExportStatusFailed
default:
return fmt.Errorf("invalid FrameworkExportStatus value: %q", val)
}
return nil
}
func (pvs FrameworkExportStatus) String() string {
var val string
switch pvs {
case FrameworkExportStatusPending:
val = "pending"
case FrameworkExportStatusProcessing:
val = "processing"
case FrameworkExportStatusCompleted:
val = "completed"
case FrameworkExportStatusFailed:
val = "failed"
default:
panic(fmt.Errorf("invalid FrameworkExportStatus value: %q", string(pvs)))
}
return val
}
func (pvs *FrameworkExportStatus) Scan(value any) error {
val, ok := value.(string)
if !ok {
return fmt.Errorf("invalid scan source for FrameworkExportStatus, expected string got %T", value)
}
return pvs.UnmarshalText([]byte(val))
}
func (pvs FrameworkExportStatus) Value() (driver.Value, error) {
return pvs.String(), nil
}

View File

@@ -249,6 +249,8 @@ WHERE %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
fmt.Printf("\n%s\n", q)
args := pgx.NamedArgs{"control_id": controlID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())

View File

@@ -0,0 +1,20 @@
CREATE TYPE framework_export_status AS ENUM (
'pending',
'processing',
'completed',
'failed'
);
CREATE TABLE framework_exports (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
framework_id TEXT NOT NULL,
status framework_export_status NOT NULL,
file_id TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
started_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE
);

View File

@@ -1097,65 +1097,14 @@ func (s *DocumentService) ExportPDF(
ctx context.Context,
documentVersionID gid.GID,
) ([]byte, error) {
document := &coredata.Document{}
version := &coredata.DocumentVersion{}
owner := &coredata.People{}
publishedBy := &coredata.People{}
signatures := coredata.DocumentVersionSignatures{}
peopleMap := make(map[gid.GID]*coredata.People)
var data []byte
err := s.svc.pg.WithConn(
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := version.LoadByID(ctx, conn, s.svc.scope, documentVersionID); err != nil {
return fmt.Errorf("cannot load document version: %w", err)
}
if err := document.LoadByID(ctx, conn, s.svc.scope, version.DocumentID); err != nil {
return fmt.Errorf("cannot load document: %w", err)
}
if version.PublishedBy != nil {
if err := publishedBy.LoadByID(ctx, conn, s.svc.scope, *version.PublishedBy); err != nil {
return fmt.Errorf("cannot load published by person: %w", err)
}
}
cursor := page.NewCursor(
100,
nil,
page.Head,
page.OrderBy[coredata.DocumentVersionSignatureOrderField]{
Field: coredata.DocumentVersionSignatureOrderFieldCreatedAt,
Direction: page.OrderDirectionAsc,
},
)
if err := signatures.LoadByDocumentVersionID(ctx, conn, s.svc.scope, documentVersionID, cursor); err != nil {
return fmt.Errorf("cannot load document version signatures: %w", err)
}
if err := owner.LoadByID(ctx, conn, s.svc.scope, document.OwnerID); err != nil {
return fmt.Errorf("cannot load document owner: %w", err)
}
// TODO: refactor this to use a single query
for _, sig := range signatures {
if _, ok := peopleMap[sig.SignedBy]; !ok {
people := &coredata.People{}
if err := people.LoadByID(ctx, conn, s.svc.scope, sig.SignedBy); err != nil {
return fmt.Errorf("cannot load people %q: %w", sig.SignedBy, err)
}
peopleMap[sig.SignedBy] = people
}
if _, ok := peopleMap[sig.RequestedBy]; !ok {
people := &coredata.People{}
if err := people.LoadByID(ctx, conn, s.svc.scope, sig.RequestedBy); err != nil {
return fmt.Errorf("cannot load people %q: %w", sig.RequestedBy, err)
}
peopleMap[sig.RequestedBy] = people
}
func(conn pg.Conn) (err error) {
data, err = exportDocumentPDF(ctx, s.html2pdfConverter, conn, s.svc.scope, documentVersionID)
if err != nil {
return fmt.Errorf("cannot export document PDF: %w", err)
}
return nil
@@ -1166,6 +1115,74 @@ func (s *DocumentService) ExportPDF(
return nil, err
}
return data, nil
}
func exportDocumentPDF(
ctx context.Context,
html2pdfConverter *html2pdf.Converter,
conn pg.Conn,
scope coredata.Scoper,
documentVersionID gid.GID,
) ([]byte, error) {
document := &coredata.Document{}
version := &coredata.DocumentVersion{}
owner := &coredata.People{}
publishedBy := &coredata.People{}
signatures := coredata.DocumentVersionSignatures{}
peopleMap := make(map[gid.GID]*coredata.People)
if err := version.LoadByID(ctx, conn, scope, documentVersionID); err != nil {
return nil, fmt.Errorf("cannot load document version: %w", err)
}
if err := document.LoadByID(ctx, conn, scope, version.DocumentID); err != nil {
return nil, fmt.Errorf("cannot load document: %w", err)
}
if version.PublishedBy != nil {
if err := publishedBy.LoadByID(ctx, conn, scope, *version.PublishedBy); err != nil {
return nil, fmt.Errorf("cannot load published by person: %w", err)
}
}
cursor := page.NewCursor(
100,
nil,
page.Head,
page.OrderBy[coredata.DocumentVersionSignatureOrderField]{
Field: coredata.DocumentVersionSignatureOrderFieldCreatedAt,
Direction: page.OrderDirectionAsc,
},
)
if err := signatures.LoadByDocumentVersionID(ctx, conn, scope, documentVersionID, cursor); err != nil {
return nil, fmt.Errorf("cannot load document version signatures: %w", err)
}
if err := owner.LoadByID(ctx, conn, scope, document.OwnerID); err != nil {
return nil, fmt.Errorf("cannot load document owner: %w", err)
}
// TODO: refactor this to use a single query
for _, sig := range signatures {
if _, ok := peopleMap[sig.SignedBy]; !ok {
people := &coredata.People{}
if err := people.LoadByID(ctx, conn, scope, sig.SignedBy); err != nil {
return nil, fmt.Errorf("cannot load people %q: %w", sig.SignedBy, err)
}
peopleMap[sig.SignedBy] = people
}
if _, ok := peopleMap[sig.RequestedBy]; !ok {
people := &coredata.People{}
if err := people.LoadByID(ctx, conn, scope, sig.RequestedBy); err != nil {
return nil, fmt.Errorf("cannot load people %q: %w", sig.RequestedBy, err)
}
peopleMap[sig.RequestedBy] = people
}
}
classification := docgen.ClassificationInternal
switch document.DocumentType {
case coredata.DocumentTypePolicy:
@@ -1212,7 +1229,7 @@ func (s *DocumentService) ExportPDF(
Scale: 1.0,
}
pdfReader, err := s.html2pdfConverter.GeneratePDF(ctx, htmlContent, cfg)
pdfReader, err := html2pdfConverter.GeneratePDF(ctx, htmlContent, cfg)
if err != nil {
return nil, fmt.Errorf("cannot generate PDF: %w", err)
}

View File

@@ -15,12 +15,17 @@
package probo
import (
"archive/zip"
"context"
"fmt"
"io"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/html2pdf"
"github.com/getprobo/probo/pkg/page"
"github.com/getprobo/probo/pkg/slug"
"github.com/getprobo/probo/pkg/soagen"
@@ -30,14 +35,12 @@ import (
const (
maxStateOfApplicabilityLimit = 10_000
maxControlsLimit = 10000
maxItemsLimit = 1000
presignExpiry = 15 * time.Minute
)
type (
FrameworkService struct {
svc *TenantService
svc *TenantService
html2pdfConverter *html2pdf.Converter
}
CreateFrameworkRequest struct {
@@ -65,6 +68,207 @@ type (
}
)
func (s FrameworkService) RequestExport(
ctx context.Context,
frameworkID gid.GID,
) error {
return s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {
framework := &coredata.Framework{}
if err := framework.LoadByID(ctx, conn, s.svc.scope, frameworkID); err != nil {
return fmt.Errorf("cannot load framework: %w", err)
}
now := time.Now()
frameworkExport := &coredata.FrameworkExport{
ID: gid.New(framework.ID.TenantID(), coredata.FrameworkExportEntityType),
FrameworkID: frameworkID,
Status: coredata.FrameworkExportStatusPending,
CreatedAt: now,
}
if err := frameworkExport.Insert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert framework export: %w", err)
}
return nil
})
}
func (s FrameworkService) Export(
ctx context.Context,
frameworkID gid.GID,
file io.Writer,
) error {
archive := zip.NewWriter(file)
defer archive.Close()
return s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
framework := &coredata.Framework{}
if err := framework.LoadByID(ctx, conn, s.svc.scope, frameworkID); err != nil {
return fmt.Errorf("cannot load framework: %w", err)
}
controls := coredata.Controls{}
err := controls.LoadByFrameworkID(
ctx,
conn,
s.svc.scope,
frameworkID,
page.NewCursor(
10_000,
nil,
page.Head,
page.OrderBy[coredata.ControlOrderField]{
Field: coredata.ControlOrderFieldSectionTitle,
Direction: page.OrderDirectionAsc,
},
),
coredata.NewControlFilter(nil),
)
if err != nil {
return fmt.Errorf("cannot load controls: %w", err)
}
for _, control := range controls {
_, err := archive.Create(fmt.Sprintf("%s/%s/", framework.Name, control.SectionTitle))
if err != nil {
return fmt.Errorf("cannot create control directory in archive: %w", err)
}
measures := coredata.Measures{}
err = measures.LoadByControlID(
ctx,
conn,
s.svc.scope,
control.ID,
page.NewCursor(
10_000,
nil,
page.Head,
page.OrderBy[coredata.MeasureOrderField]{
Field: coredata.MeasureOrderFieldCreatedAt,
Direction: page.OrderDirectionAsc,
},
),
coredata.NewMeasureFilter(nil),
)
if err != nil {
return fmt.Errorf("cannot load measures: %w", err)
}
for _, measure := range measures {
_, err := archive.Create(fmt.Sprintf("%s/%s/%s/", framework.Name, control.SectionTitle, measure.Name))
if err != nil {
return fmt.Errorf("cannot create measure directory in archive: %w", err)
}
evidences := coredata.Evidences{}
err = evidences.LoadByMeasureID(
ctx,
conn,
s.svc.scope,
measure.ID,
page.NewCursor(
10_000,
nil,
page.Head,
page.OrderBy[coredata.EvidenceOrderField]{
Field: coredata.EvidenceOrderFieldCreatedAt,
Direction: page.OrderDirectionAsc,
},
),
)
if err != nil {
return fmt.Errorf("cannot load evidences: %w", err)
}
for _, evidence := range evidences {
object, err := s.svc.s3.GetObject(
ctx,
&s3.GetObjectInput{
Bucket: aws.String(s.svc.bucket),
Key: aws.String(evidence.ObjectKey),
},
)
if err != nil {
return fmt.Errorf("cannot download evidence: %w", err)
}
defer object.Body.Close()
if err != nil {
return fmt.Errorf("cannot download evidence: %w", err)
}
w, err := archive.Create(fmt.Sprintf("%s/%s/%s/%s", framework.Name, control.SectionTitle, measure.Name, evidence.Filename))
if err != nil {
return fmt.Errorf("cannot create evidence in archive: %w", err)
}
_, err = io.Copy(w, object.Body)
if err != nil {
return fmt.Errorf("cannot write evidence to archive: %w", err)
}
}
}
documents := coredata.Documents{}
err = documents.LoadByControlID(
ctx,
conn,
s.svc.scope,
control.ID,
page.NewCursor(
10_000,
nil,
page.Head,
page.OrderBy[coredata.DocumentOrderField]{
Field: coredata.DocumentOrderFieldCreatedAt,
Direction: page.OrderDirectionAsc,
},
),
coredata.NewDocumentFilter(nil),
)
if err != nil {
return fmt.Errorf("cannot load documents: %w", err)
}
for _, document := range documents {
documentVersion := &coredata.DocumentVersion{}
if err := documentVersion.LoadLatestPublishedVersion(ctx, conn, s.svc.scope, document.ID); err != nil {
return fmt.Errorf("cannot load document version: %w", err)
}
exportedPDF, err := exportDocumentPDF(
ctx,
s.html2pdfConverter,
conn,
s.svc.scope,
documentVersion.ID,
)
if err != nil {
return fmt.Errorf("cannot export document PDF: %w", err)
}
w, err := archive.Create(fmt.Sprintf("%s/%s/%s.pdf", framework.Name, control.SectionTitle, document.Title))
if err != nil {
return fmt.Errorf("cannot create document in archive: %w", err)
}
_, err = w.Write(exportedPDF)
if err != nil {
return fmt.Errorf("cannot write document to archive: %w", err)
}
}
}
return nil
},
)
}
func (s FrameworkService) Create(
ctx context.Context,
req CreateFrameworkRequest,

View File

@@ -17,6 +17,7 @@ package probo
import (
"context"
"fmt"
"os"
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
@@ -27,7 +28,9 @@ import (
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/html2pdf"
"github.com/getprobo/probo/pkg/usrmgr"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.gearno.de/x/ref"
)
type (
@@ -136,7 +139,10 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
agent: agents.NewAgent(nil, s.agentConfig),
}
tenantService.Frameworks = &FrameworkService{svc: tenantService}
tenantService.Frameworks = &FrameworkService{
svc: tenantService,
html2pdfConverter: s.html2pdfConverter,
}
tenantService.Measures = &MeasureService{svc: tenantService}
tenantService.Tasks = &TaskService{svc: tenantService}
tenantService.Evidences = &EvidenceService{
@@ -184,3 +190,144 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
tenantService.ProcessingActivityRegistries = &ProcessingActivityRegistryService{svc: tenantService}
return tenantService
}
func (s *Service) ExportFrameworkJob(ctx context.Context) error {
return s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
fe := &coredata.FrameworkExport{}
if err := fe.LoadNextPendingForUpdateSkipLocked(ctx, tx); err != nil {
return err
}
scope := coredata.NewScope(fe.ID.TenantID())
framework := &coredata.Framework{}
if err := framework.LoadByID(ctx, tx, scope, fe.FrameworkID); err != nil {
fe.Status = coredata.FrameworkExportStatusFailed
fe.CompletedAt = ref.Ref(time.Now())
if err := fe.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update framework export: %w", err)
}
// s.logger.Error(ctx, "cannot load framework", "error", err)
return nil
}
tenantService := s.WithTenant(fe.ID.TenantID())
tempDir := os.TempDir()
tempFile, err := os.CreateTemp(tempDir, "probo-framework-export-*.zip")
if err != nil {
fe.Status = coredata.FrameworkExportStatusFailed
fe.CompletedAt = ref.Ref(time.Now())
if err := fe.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update framework export: %w", err)
}
// s.logger.Error(ctx, "cannot create temp file", "error", err)
return nil
}
defer tempFile.Close()
defer os.Remove(tempFile.Name())
err = tenantService.Frameworks.Export(ctx, fe.FrameworkID, tempFile)
if err != nil {
fe.Status = coredata.FrameworkExportStatusFailed
fe.CompletedAt = ref.Ref(time.Now())
if err := fe.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update framework export: %w", err)
}
// s.logger.Error(ctx, "cannot export framework", "error", err)
return nil
}
uuid, err := uuid.NewV4()
if err != nil {
fe.Status = coredata.FrameworkExportStatusFailed
fe.CompletedAt = ref.Ref(time.Now())
if err := fe.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update framework export: %w", err)
}
// s.logger.Error(ctx, "cannot generate UUID", "error", err)
return nil
}
if _, err := tempFile.Seek(0, 0); err != nil {
fe.Status = coredata.FrameworkExportStatusFailed
fe.CompletedAt = ref.Ref(time.Now())
if err := fe.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update framework export: %w", err)
}
// s.logger.Error(ctx, "cannot seek temp file", "error", err)
return nil
}
fileInfo, err := tempFile.Stat()
if err != nil {
fe.Status = coredata.FrameworkExportStatusFailed
fe.CompletedAt = ref.Ref(time.Now())
if err := fe.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update framework export: %w", err)
}
// s.logger.Error(ctx, "cannot get temp file info", "error", err)
return nil
}
_, err = s.s3.PutObject(
ctx,
&s3.PutObjectInput{
Bucket: ref.Ref(s.bucket),
Key: ref.Ref(uuid.String()),
Body: tempFile,
ContentLength: ref.Ref(fileInfo.Size()),
ContentType: ref.Ref("application/zip"),
Metadata: map[string]string{
"framework-id": framework.ID.String(),
"framework-export-id": fe.ID.String(),
},
},
)
if err != nil {
fe.Status = coredata.FrameworkExportStatusFailed
fe.CompletedAt = ref.Ref(time.Now())
if err := fe.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update framework export: %w", err)
}
// s.logger.Error(ctx, "cannot upload file to S3", "error", err)
return nil
}
now := time.Now()
file := coredata.File{
ID: gid.New(fe.ID.TenantID(), coredata.FileEntityType),
BucketName: s.bucket,
MimeType: "application/zip",
FileName: fmt.Sprintf("%s Archive %s.zip", framework.Name, time.Now().Format("2006-01-02")),
FileKey: uuid.String(),
CreatedAt: now,
UpdatedAt: now,
}
if err := file.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert file: %w", err)
}
fe.FileID = &file.ID
fe.CompletedAt = &now
fe.Status = coredata.FrameworkExportStatusCompleted
if err := fe.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update framework export: %w", err)
}
return nil
},
)
}

View File

@@ -2265,6 +2265,7 @@ type Mutation {
generateFrameworkStateOfApplicability(
input: GenerateFrameworkStateOfApplicabilityInput!
): GenerateFrameworkStateOfApplicabilityPayload!
exportFramework(input: ExportFrameworkInput!): ExportFrameworkPayload!
# Control mutations
createControl(input: CreateControlInput!): CreateControlPayload!
@@ -2656,6 +2657,10 @@ input DeleteFrameworkInput {
frameworkId: ID!
}
input ExportFrameworkInput {
frameworkId: ID!
}
input CreateMeasureInput {
organizationId: ID!
name: String!
@@ -3219,6 +3224,10 @@ type DeleteFrameworkPayload {
deletedFrameworkId: ID!
}
type ExportFrameworkPayload {
exportJobId: ID!
}
type CreateMeasurePayload {
measureEdge: MeasureEdge!
}

View File

@@ -663,6 +663,10 @@ type ComplexityRoot struct {
Data func(childComplexity int) int
}
ExportFrameworkPayload struct {
ExportJobID func(childComplexity int) int
}
Framework struct {
Controls func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) int
CreatedAt func(childComplexity int) int
@@ -801,6 +805,7 @@ type ComplexityRoot struct {
DeleteVendorDataPrivacyAgreement func(childComplexity int, input types.DeleteVendorDataPrivacyAgreementInput) int
DeleteVendorService func(childComplexity int, input types.DeleteVendorServiceInput) int
ExportDocumentVersionPDF func(childComplexity int, input types.ExportDocumentVersionPDFInput) int
ExportFramework func(childComplexity int, input types.ExportFrameworkInput) int
FulfillEvidence func(childComplexity int, input types.FulfillEvidenceInput) int
GenerateDocumentChangelog func(childComplexity int, input types.GenerateDocumentChangelogInput) int
GenerateFrameworkStateOfApplicability func(childComplexity int, input types.GenerateFrameworkStateOfApplicabilityInput) int
@@ -1570,6 +1575,7 @@ type MutationResolver interface {
ImportFramework(ctx context.Context, input types.ImportFrameworkInput) (*types.ImportFrameworkPayload, error)
DeleteFramework(ctx context.Context, input types.DeleteFrameworkInput) (*types.DeleteFrameworkPayload, error)
GenerateFrameworkStateOfApplicability(ctx context.Context, input types.GenerateFrameworkStateOfApplicabilityInput) (*types.GenerateFrameworkStateOfApplicabilityPayload, error)
ExportFramework(ctx context.Context, input types.ExportFrameworkInput) (*types.ExportFrameworkPayload, error)
CreateControl(ctx context.Context, input types.CreateControlInput) (*types.CreateControlPayload, error)
UpdateControl(ctx context.Context, input types.UpdateControlInput) (*types.UpdateControlPayload, error)
DeleteControl(ctx context.Context, input types.DeleteControlInput) (*types.DeleteControlPayload, error)
@@ -3681,6 +3687,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.ExportDocumentVersionPDFPayload.Data(childComplexity), true
case "ExportFrameworkPayload.exportJobId":
if e.complexity.ExportFrameworkPayload.ExportJobID == nil {
break
}
return e.complexity.ExportFrameworkPayload.ExportJobID(childComplexity), true
case "Framework.controls":
if e.complexity.Framework.Controls == nil {
break
@@ -4748,6 +4761,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Mutation.ExportDocumentVersionPDF(childComplexity, args["input"].(types.ExportDocumentVersionPDFInput)), true
case "Mutation.exportFramework":
if e.complexity.Mutation.ExportFramework == nil {
break
}
args, err := ec.field_Mutation_exportFramework_args(ctx, rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.ExportFramework(childComplexity, args["input"].(types.ExportFrameworkInput)), true
case "Mutation.fulfillEvidence":
if e.complexity.Mutation.FulfillEvidence == nil {
break
@@ -7806,6 +7831,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputDocumentVersionSignatureOrder,
ec.unmarshalInputEvidenceOrder,
ec.unmarshalInputExportDocumentVersionPDFInput,
ec.unmarshalInputExportFrameworkInput,
ec.unmarshalInputFrameworkOrder,
ec.unmarshalInputFulfillEvidenceInput,
ec.unmarshalInputGenerateDocumentChangelogInput,
@@ -10234,6 +10260,7 @@ type Mutation {
generateFrameworkStateOfApplicability(
input: GenerateFrameworkStateOfApplicabilityInput!
): GenerateFrameworkStateOfApplicabilityPayload!
exportFramework(input: ExportFrameworkInput!): ExportFrameworkPayload!
# Control mutations
createControl(input: CreateControlInput!): CreateControlPayload!
@@ -10625,6 +10652,10 @@ input DeleteFrameworkInput {
frameworkId: ID!
}
input ExportFrameworkInput {
frameworkId: ID!
}
input CreateMeasureInput {
organizationId: ID!
name: String!
@@ -11188,6 +11219,10 @@ type DeleteFrameworkPayload {
deletedFrameworkId: ID!
}
type ExportFrameworkPayload {
exportJobId: ID!
}
type CreateMeasurePayload {
measureEdge: MeasureEdge!
}
@@ -14952,6 +14987,29 @@ func (ec *executionContext) field_Mutation_exportDocumentVersionPDF_argsInput(
return zeroVal, nil
}
func (ec *executionContext) field_Mutation_exportFramework_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
arg0, err := ec.field_Mutation_exportFramework_argsInput(ctx, rawArgs)
if err != nil {
return nil, err
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_exportFramework_argsInput(
ctx context.Context,
rawArgs map[string]any,
) (types.ExportFrameworkInput, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
if tmp, ok := rawArgs["input"]; ok {
return ec.unmarshalNExportFrameworkInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐExportFrameworkInput(ctx, tmp)
}
var zeroVal types.ExportFrameworkInput
return zeroVal, nil
}
func (ec *executionContext) field_Mutation_fulfillEvidence_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
@@ -32208,6 +32266,50 @@ func (ec *executionContext) fieldContext_ExportDocumentVersionPDFPayload_data(_
return fc, nil
}
func (ec *executionContext) _ExportFrameworkPayload_exportJobId(ctx context.Context, field graphql.CollectedField, obj *types.ExportFrameworkPayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_ExportFrameworkPayload_exportJobId(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.ExportJobID, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(gid.GID)
fc.Result = res
return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_ExportFrameworkPayload_exportJobId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "ExportFrameworkPayload",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type ID does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Framework_id(ctx context.Context, field graphql.CollectedField, obj *types.Framework) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Framework_id(ctx, field)
if err != nil {
@@ -35532,6 +35634,65 @@ func (ec *executionContext) fieldContext_Mutation_generateFrameworkStateOfApplic
return fc, nil
}
func (ec *executionContext) _Mutation_exportFramework(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_exportFramework(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().ExportFramework(rctx, fc.Args["input"].(types.ExportFrameworkInput))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*types.ExportFrameworkPayload)
fc.Result = res
return ec.marshalNExportFrameworkPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐExportFrameworkPayload(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Mutation_exportFramework(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Mutation",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "exportJobId":
return ec.fieldContext_ExportFrameworkPayload_exportJobId(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type ExportFrameworkPayload", field.Name)
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_exportFramework_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
return fc, nil
}
func (ec *executionContext) _Mutation_createControl(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_createControl(ctx, field)
if err != nil {
@@ -63306,6 +63467,33 @@ func (ec *executionContext) unmarshalInputExportDocumentVersionPDFInput(ctx cont
return it, nil
}
func (ec *executionContext) unmarshalInputExportFrameworkInput(ctx context.Context, obj any) (types.ExportFrameworkInput, error) {
var it types.ExportFrameworkInput
asMap := map[string]any{}
for k, v := range obj.(map[string]any) {
asMap[k] = v
}
fieldsInOrder := [...]string{"frameworkId"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
case "frameworkId":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("frameworkId"))
data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v)
if err != nil {
return it, err
}
it.FrameworkID = data
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputFrameworkOrder(ctx context.Context, obj any) (types.FrameworkOrderBy, error) {
var it types.FrameworkOrderBy
asMap := map[string]any{}
@@ -72518,6 +72706,45 @@ func (ec *executionContext) _ExportDocumentVersionPDFPayload(ctx context.Context
return out
}
var exportFrameworkPayloadImplementors = []string{"ExportFrameworkPayload"}
func (ec *executionContext) _ExportFrameworkPayload(ctx context.Context, sel ast.SelectionSet, obj *types.ExportFrameworkPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, exportFrameworkPayloadImplementors)
out := graphql.NewFieldSet(fields)
deferred := make(map[string]*graphql.FieldSet)
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("ExportFrameworkPayload")
case "exportJobId":
out.Values[i] = ec._ExportFrameworkPayload_exportJobId(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch(ctx)
if out.Invalids > 0 {
return graphql.Null
}
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
for label, dfs := range deferred {
ec.processDeferredGroup(graphql.DeferredGroup{
Label: label,
Path: graphql.GetPath(ctx),
FieldSet: dfs,
Context: ctx,
})
}
return out
}
var frameworkImplementors = []string{"Framework", "Node"}
func (ec *executionContext) _Framework(ctx context.Context, sel ast.SelectionSet, obj *types.Framework) graphql.Marshaler {
@@ -73552,6 +73779,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "exportFramework":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_exportFramework(ctx, field)
})
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "createControl":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_createControl(ctx, field)
@@ -83960,6 +84194,25 @@ func (ec *executionContext) marshalNExportDocumentVersionPDFPayload2ᚖgithubᚗ
return ec._ExportDocumentVersionPDFPayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNExportFrameworkInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐExportFrameworkInput(ctx context.Context, v any) (types.ExportFrameworkInput, error) {
res, err := ec.unmarshalInputExportFrameworkInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNExportFrameworkPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐExportFrameworkPayload(ctx context.Context, sel ast.SelectionSet, v types.ExportFrameworkPayload) graphql.Marshaler {
return ec._ExportFrameworkPayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNExportFrameworkPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐExportFrameworkPayload(ctx context.Context, sel ast.SelectionSet, v *types.ExportFrameworkPayload) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
}
return graphql.Null
}
return ec._ExportFrameworkPayload(ctx, sel, v)
}
func (ec *executionContext) marshalNFramework2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐFramework(ctx context.Context, sel ast.SelectionSet, v types.Framework) graphql.Marshaler {
return ec._Framework(ctx, sel, &v)
}

View File

@@ -1029,6 +1029,14 @@ type ExportDocumentVersionPDFPayload struct {
Data string `json:"data"`
}
type ExportFrameworkInput struct {
FrameworkID gid.GID `json:"frameworkId"`
}
type ExportFrameworkPayload struct {
ExportJobID gid.GID `json:"exportJobId"`
}
type Framework struct {
ID gid.GID `json:"id"`
Name string `json:"name"`

View File

@@ -1678,6 +1678,11 @@ func (r *mutationResolver) GenerateFrameworkStateOfApplicability(ctx context.Con
}, nil
}
// ExportFramework is the resolver for the exportFramework field.
func (r *mutationResolver) ExportFramework(ctx context.Context, input types.ExportFrameworkInput) (*types.ExportFrameworkPayload, error) {
panic(fmt.Errorf("not implemented: ExportFramework - exportFramework"))
}
// CreateControl is the resolver for the createControl field.
func (r *mutationResolver) CreateControl(ctx context.Context, input types.CreateControlInput) (*types.CreateControlPayload, error) {
prb := r.ProboService(ctx, input.FrameworkID.TenantID())
@@ -3958,6 +3963,10 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
return types.NewVendor(vendor), nil
case coredata.FrameworkEntityType:
if err := prb.Frameworks.Export(ctx, id, nil); err != nil {
panic(fmt.Errorf("cannot export organization frameworks: %w", err))
}
framework, err := prb.Frameworks.Get(ctx, id)
if err != nil {
panic(fmt.Errorf("cannot get framework: %w", err))