diff --git a/pkg/coredata/document_version_approval_quorum.go b/pkg/coredata/document_version_approval_quorum.go index 03db34415e..5597ee9fab 100644 --- a/pkg/coredata/document_version_approval_quorum.go +++ b/pkg/coredata/document_version_approval_quorum.go @@ -37,12 +37,15 @@ import ( type ( DocumentVersionApprovalQuorum struct { - ID gid.GID `db:"id"` - OrganizationID gid.GID `db:"organization_id"` - VersionID gid.GID `db:"version_id"` - Status DocumentVersionApprovalQuorumStatus `db:"status"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + ID gid.GID `db:"id"` + OrganizationID gid.GID `db:"organization_id"` + VersionID gid.GID `db:"version_id"` + FileID *gid.GID `db:"file_id"` + PdfAttemptCount int `db:"pdf_attempt_count"` + PdfClaimedAt *time.Time `db:"pdf_claimed_at"` + Status DocumentVersionApprovalQuorumStatus `db:"status"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } DocumentVersionApprovalQuorums []*DocumentVersionApprovalQuorum @@ -107,6 +110,9 @@ SELECT id, organization_id, version_id, + file_id, + pdf_attempt_count, + pdf_claimed_at, status, created_at, updated_at @@ -152,6 +158,9 @@ SELECT id, organization_id, version_id, + file_id, + pdf_attempt_count, + pdf_claimed_at, status, created_at, updated_at @@ -204,6 +213,9 @@ SELECT document_version_approval_quorums.id, document_version_approval_quorums.organization_id, document_version_approval_quorums.version_id, + document_version_approval_quorums.file_id, + document_version_approval_quorums.pdf_attempt_count, + document_version_approval_quorums.pdf_claimed_at, document_version_approval_quorums.status, document_version_approval_quorums.created_at, document_version_approval_quorums.updated_at @@ -259,6 +271,9 @@ SELECT document_version_approval_quorums.id, document_version_approval_quorums.organization_id, document_version_approval_quorums.version_id, + document_version_approval_quorums.file_id, + document_version_approval_quorums.pdf_attempt_count, + document_version_approval_quorums.pdf_claimed_at, document_version_approval_quorums.status, document_version_approval_quorums.created_at, document_version_approval_quorums.updated_at @@ -340,6 +355,9 @@ INSERT INTO document_version_approval_quorums ( tenant_id, organization_id, version_id, + file_id, + pdf_attempt_count, + pdf_claimed_at, status, created_at, updated_at @@ -348,6 +366,9 @@ INSERT INTO document_version_approval_quorums ( @tenant_id, @organization_id, @version_id, + @file_id, + @pdf_attempt_count, + @pdf_claimed_at, @status, @created_at, @updated_at @@ -355,13 +376,16 @@ INSERT INTO document_version_approval_quorums ( ` args := pgx.StrictNamedArgs{ - "id": q.ID, - "tenant_id": scope.GetTenantID(), - "organization_id": q.OrganizationID, - "version_id": q.VersionID, - "status": q.Status, - "created_at": q.CreatedAt, - "updated_at": q.UpdatedAt, + "id": q.ID, + "tenant_id": scope.GetTenantID(), + "organization_id": q.OrganizationID, + "version_id": q.VersionID, + "file_id": q.FileID, + "pdf_attempt_count": q.PdfAttemptCount, + "pdf_claimed_at": q.PdfClaimedAt, + "status": q.Status, + "created_at": q.CreatedAt, + "updated_at": q.UpdatedAt, } _, err := conn.Exec(ctx, query, args) @@ -435,3 +459,228 @@ WHERE return nil } + +func (q *DocumentVersionApprovalQuorum) ClaimNextWithoutFileForUpdate( + ctx context.Context, + conn pg.Tx, + maxAttempts int, + now time.Time, + lease time.Duration, +) error { + query := ` +SELECT + q.id, + q.organization_id, + q.version_id, + q.file_id, + q.pdf_attempt_count, + q.pdf_claimed_at, + q.status, + q.created_at, + q.updated_at +FROM + document_version_approval_quorums q +INNER JOIN + document_versions dv ON dv.id = q.version_id AND dv.tenant_id = q.tenant_id +INNER JOIN + documents d ON d.id = dv.document_id AND d.tenant_id = q.tenant_id +WHERE + q.file_id IS NULL + AND q.pdf_attempt_count < @max_pdf_attempts + AND q.status = @status + AND d.deleted_at IS NULL + AND ( + q.pdf_claimed_at IS NULL + OR q.pdf_claimed_at < @claimed_before + ) +ORDER BY q.created_at ASC +LIMIT 1 +FOR UPDATE OF q SKIP LOCKED +` + + rows, err := conn.Query( + ctx, + query, + pgx.StrictNamedArgs{ + "max_pdf_attempts": maxAttempts, + "status": DocumentVersionApprovalQuorumStatusPending, + "claimed_before": now.Add(-lease), + }, + ) + if err != nil { + return fmt.Errorf("cannot query approval quorums: %w", err) + } + + result, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[DocumentVersionApprovalQuorum]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrNoDocumentPDFJobAvailable + } + + return fmt.Errorf("cannot collect approval quorum: %w", err) + } + + result.PdfAttemptCount++ + result.PdfClaimedAt = new(now) + result.UpdatedAt = now + + updateQuery := ` +UPDATE document_version_approval_quorums SET + pdf_attempt_count = @pdf_attempt_count, + pdf_claimed_at = @pdf_claimed_at, + updated_at = @updated_at +WHERE + tenant_id = @tenant_id + AND id = @id +` + + _, err = conn.Exec( + ctx, + updateQuery, + pgx.StrictNamedArgs{ + "id": result.ID, + "tenant_id": result.ID.TenantID(), + "pdf_attempt_count": result.PdfAttemptCount, + "pdf_claimed_at": result.PdfClaimedAt, + "updated_at": result.UpdatedAt, + }, + ) + if err != nil { + return fmt.Errorf("cannot mark approval quorum as generating PDF: %w", err) + } + + *q = result + + return nil +} + +func (q *DocumentVersionApprovalQuorum) HasPDFClaim( + ctx context.Context, + conn pg.Querier, + scope Scoper, +) (bool, error) { + if q.PdfClaimedAt == nil { + return false, nil + } + + query := ` +SELECT EXISTS ( + SELECT 1 + FROM document_version_approval_quorums + WHERE + %s + AND id = @id + AND file_id IS NULL + AND status = @status + AND pdf_claimed_at = @pdf_claimed_at +) +` + + query = fmt.Sprintf(query, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "id": q.ID, + "status": DocumentVersionApprovalQuorumStatusPending, + "pdf_claimed_at": q.PdfClaimedAt, + } + maps.Copy(args, scope.SQLArguments()) + + var exists bool + if err := conn.QueryRow(ctx, query, args).Scan(&exists); err != nil { + return false, fmt.Errorf("cannot check approval quorum PDF claim: %w", err) + } + + return exists, nil +} + +func (q *DocumentVersionApprovalQuorum) AttachPDFFile( + ctx context.Context, + conn pg.Tx, + scope Scoper, + fileID gid.GID, + now time.Time, +) (bool, error) { + if q.PdfClaimedAt == nil { + return false, nil + } + + query := ` +UPDATE document_version_approval_quorums +SET + file_id = @file_id, + updated_at = @updated_at +WHERE + %s + AND id = @id + AND file_id IS NULL + AND status = @status + AND pdf_claimed_at = @pdf_claimed_at +` + + query = fmt.Sprintf(query, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "id": q.ID, + "file_id": fileID, + "status": DocumentVersionApprovalQuorumStatusPending, + "pdf_claimed_at": q.PdfClaimedAt, + "updated_at": now, + } + maps.Copy(args, scope.SQLArguments()) + + commandTag, err := conn.Exec(ctx, query, args) + if err != nil { + return false, fmt.Errorf("cannot attach approval quorum PDF: %w", err) + } + + if commandTag.RowsAffected() == 0 { + return false, nil + } + + q.FileID = new(fileID) + q.UpdatedAt = now + + return true, nil +} + +func (q *DocumentVersionApprovalQuorum) ReleasePDFClaim( + ctx context.Context, + conn pg.Tx, + scope Scoper, + now time.Time, +) error { + if q.PdfClaimedAt == nil { + return nil + } + + query := ` +UPDATE document_version_approval_quorums +SET + pdf_claimed_at = NULL, + updated_at = @updated_at +WHERE + %s + AND id = @id + AND file_id IS NULL + AND pdf_claimed_at = @pdf_claimed_at +` + + query = fmt.Sprintf(query, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "id": q.ID, + "pdf_claimed_at": q.PdfClaimedAt, + "updated_at": now, + } + maps.Copy(args, scope.SQLArguments()) + + _, err := conn.Exec(ctx, query, args) + if err != nil { + return fmt.Errorf("cannot release approval quorum PDF claim: %w", err) + } + + q.PdfClaimedAt = nil + q.UpdatedAt = now + + return nil +} diff --git a/pkg/coredata/migrations/20260828T112305Z.sql b/pkg/coredata/migrations/20260828T112305Z.sql new file mode 100644 index 0000000000..0410db2a3d --- /dev/null +++ b/pkg/coredata/migrations/20260828T112305Z.sql @@ -0,0 +1,27 @@ +-- Copyright (c) 2026 Probo Inc . +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy +-- of this software and associated documentation files (the "Software"), to deal +-- in the Software without restriction, including without limitation the rights +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +-- copies of the Software, and to permit persons to whom the Software is +-- furnished to do so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in +-- all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. + +ALTER TABLE document_version_approval_quorums + ADD COLUMN file_id TEXT REFERENCES files(id), + ADD COLUMN pdf_attempt_count INT NOT NULL DEFAULT 0, + ADD COLUMN pdf_claimed_at TIMESTAMPTZ; + +ALTER TABLE document_version_approval_quorums + ALTER COLUMN pdf_attempt_count DROP DEFAULT; diff --git a/pkg/probo/document_approval_quorum_pdf_worker.go b/pkg/probo/document_approval_quorum_pdf_worker.go new file mode 100644 index 0000000000..c0a24c3e7f --- /dev/null +++ b/pkg/probo/document_approval_quorum_pdf_worker.go @@ -0,0 +1,122 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package probo + +import ( + "context" + "errors" + "time" + + "go.gearno.de/kit/log" + "go.gearno.de/kit/pg" + "go.gearno.de/kit/worker" + "go.probo.inc/probo/pkg/coredata" +) + +const pdfClaimLease = 5 * time.Minute + +type documentApprovalQuorumPDFHandler struct { + service *Service + logger *log.Logger +} + +func NewDocumentApprovalQuorumPDFWorker( + service *Service, + logger *log.Logger, + opts ...worker.Option, +) *worker.Worker[coredata.DocumentVersionApprovalQuorum] { + h := &documentApprovalQuorumPDFHandler{ + service: service, + logger: logger, + } + + return worker.New( + "document-approval-quorum-pdf-worker", + h, + logger, + opts..., + ) +} + +func (h *documentApprovalQuorumPDFHandler) Claim(ctx context.Context) (coredata.DocumentVersionApprovalQuorum, error) { + var quorum coredata.DocumentVersionApprovalQuorum + + if err := h.service.pg.WithTx( + ctx, + func(ctx context.Context, tx pg.Tx) error { + return quorum.ClaimNextWithoutFileForUpdate( + ctx, + tx, + maxPDFAttempts, + time.Now(), + pdfClaimLease, + ) + }, + ); err != nil { + if errors.Is(err, coredata.ErrNoDocumentPDFJobAvailable) { + return coredata.DocumentVersionApprovalQuorum{}, worker.ErrNoTask + } + + return coredata.DocumentVersionApprovalQuorum{}, err + } + + return quorum, nil +} + +func (h *documentApprovalQuorumPDFHandler) Process(ctx context.Context, quorum coredata.DocumentVersionApprovalQuorum) error { + scope := coredata.NewScope(quorum.ID.TenantID()) + + if err := h.service.DocumentApprovals.generateAndUploadQuorumPDF(ctx, scope, &quorum); err != nil { + h.logger.ErrorCtx( + ctx, + "document approval quorum pdf worker failure", + log.Error(err), + log.String("approval_quorum_id", quorum.ID.String()), + log.Int("attempt", quorum.PdfAttemptCount), + ) + + if releaseErr := h.releasePDFClaim(ctx, scope, &quorum); releaseErr != nil { + h.logger.ErrorCtx( + ctx, + "cannot release document approval quorum pdf claim", + log.Error(releaseErr), + log.String("approval_quorum_id", quorum.ID.String()), + ) + } + + return err + } + + return nil +} + +func (h *documentApprovalQuorumPDFHandler) releasePDFClaim( + ctx context.Context, + scope coredata.Scoper, + quorum *coredata.DocumentVersionApprovalQuorum, +) error { + return h.service.pg.WithTx( + ctx, + func(ctx context.Context, tx pg.Tx) error { + return quorum.ReleasePDFClaim(ctx, tx, scope, time.Now()) + }, + ) +} diff --git a/pkg/probo/document_approval_service.go b/pkg/probo/document_approval_service.go index 2d5e76f57e..52fdd12cfc 100644 --- a/pkg/probo/document_approval_service.go +++ b/pkg/probo/document_approval_service.go @@ -75,6 +75,8 @@ func (e ErrApprovalDecisionAlreadyMade) Error() string { return "approval decision has already been made" } +var errQuorumPDFClaimLost = errors.New("approval quorum pdf claim lost") + func (s *DocumentApprovalService) RequestApproval( ctx context.Context, scope coredata.Scoper, @@ -1011,6 +1013,148 @@ func (s *DocumentApprovalService) generateApprovalPDF( return pdfData, err } +func (s *DocumentApprovalService) generateAndUploadQuorumPDF( + ctx context.Context, + scope coredata.Scoper, + quorum *coredata.DocumentVersionApprovalQuorum, +) error { + if quorum == nil || quorum.FileID != nil || quorum.PdfClaimedAt == nil { + return nil + } + + var ( + pdfInput *documentPDFInput + skip bool + ) + + err := s.svc.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + holdsClaim, err := quorum.HasPDFClaim(ctx, conn, scope) + if err != nil { + return err + } + + if !holdsClaim { + skip = true + + return nil + } + + version := &coredata.DocumentVersion{} + if err := version.LoadByID(ctx, conn, scope, quorum.VersionID); err != nil { + return fmt.Errorf("cannot load document version: %w", err) + } + + pdfInput, err = loadDocumentPDFInput(ctx, conn, scope, version) + if err != nil { + return err + } + + return nil + }, + ) + if err != nil { + return fmt.Errorf("cannot load quorum document PDF data: %w", err) + } + + if skip { + return nil + } + + pdfData, err := renderDocumentPDF( + ctx, + s.svc, + s.html2pdfConverter, + pdfInput, + ExportPDFOptions{}, + ) + if err != nil { + return fmt.Errorf("cannot generate quorum document PDF: %w", err) + } + + err = s.svc.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + holdsClaim, err := quorum.HasPDFClaim(ctx, conn, scope) + if err != nil { + return err + } + + if !holdsClaim { + skip = true + } + + return nil + }, + ) + if err != nil { + return fmt.Errorf("cannot load approval quorum: %w", err) + } + + if skip { + return nil + } + + now := time.Now() + + fileRecord := &coredata.File{ + ID: gid.New(scope.GetTenantID(), coredata.FileEntityType), + OrganizationID: quorum.OrganizationID, + BucketName: s.svc.bucket, + MimeType: "application/pdf", + FileName: fmt.Sprintf("approval-quorum-%s.pdf", quorum.ID), + FileKey: uuid.MustNewV4().String(), + Visibility: coredata.FileVisibilityPrivate, + CreatedAt: now, + UpdatedAt: now, + } + + fileSize, err := s.svc.fileManager.PutFile( + ctx, + fileRecord, + bytes.NewReader(pdfData), + map[string]string{ + "type": "approval-quorum-document", + "quorum-id": quorum.ID.String(), + }, + ) + if err != nil { + return fmt.Errorf("cannot upload quorum document PDF: %w", err) + } + + fileRecord.FileSize = fileSize + + err = s.svc.pg.WithTx( + ctx, + func(ctx context.Context, tx pg.Tx) error { + if err := fileRecord.Insert(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot insert quorum document file: %w", err) + } + + attached, err := quorum.AttachPDFFile(ctx, tx, scope, fileRecord.ID, now) + if err != nil { + return fmt.Errorf("cannot attach quorum document file: %w", err) + } + + if !attached { + return errQuorumPDFClaimLost + } + + return nil + }, + ) + if err != nil { + if errors.Is(err, errQuorumPDFClaimLost) { + return nil + } + + return err + } + + return nil +} + func (s *DocumentApprovalService) countDecisions( ctx context.Context, scope coredata.Scoper, conn pg.Querier, diff --git a/pkg/probo/document_service.go b/pkg/probo/document_service.go index 713398a38a..ebed6101af 100644 --- a/pkg/probo/document_service.go +++ b/pkg/probo/document_service.go @@ -2878,6 +2878,12 @@ func generateSignaturePagePDF( return pdfData, nil } +type documentPDFInput struct { + version *coredata.DocumentVersion + approverNames []string + horizontalLogoFile *coredata.File +} + func generateDocumentPDF( ctx context.Context, svc *Service, @@ -2887,6 +2893,20 @@ func generateDocumentPDF( version *coredata.DocumentVersion, options ExportPDFOptions, ) ([]byte, error) { + input, err := loadDocumentPDFInput(ctx, conn, scope, version) + if err != nil { + return nil, err + } + + return renderDocumentPDF(ctx, svc, html2pdfConverter, input, options) +} + +func loadDocumentPDFInput( + ctx context.Context, + conn pg.Querier, + scope coredata.Scoper, + version *coredata.DocumentVersion, +) (*documentPDFInput, error) { document := &coredata.Document{} organization := &coredata.Organization{} @@ -2949,6 +2969,31 @@ func generateDocumentPDF( return nil, fmt.Errorf("cannot load organization: %w", err) } + var horizontalLogoFile *coredata.File + + if organization.HorizontalLogoFileID != nil { + fileRecord := &coredata.File{} + if err := fileRecord.LoadByID(ctx, conn, scope, *organization.HorizontalLogoFileID); err == nil { + horizontalLogoFile = fileRecord + } + } + + return &documentPDFInput{ + version: version, + approverNames: approverNames, + horizontalLogoFile: horizontalLogoFile, + }, nil +} + +func renderDocumentPDF( + ctx context.Context, + svc *Service, + html2pdfConverter *html2pdf.Converter, + input *documentPDFInput, + options ExportPDFOptions, +) ([]byte, error) { + version := input.version + classification := docgen.ClassificationSecret switch version.Classification { @@ -2962,15 +3007,10 @@ func generateDocumentPDF( horizontalLogoBase64 := "" - if organization.HorizontalLogoFileID != nil { - fileRecord := &coredata.File{} - - fileErr := fileRecord.LoadByID(ctx, conn, scope, *organization.HorizontalLogoFileID) - if fileErr == nil { - base64Data, mimeType, logoErr := svc.fileManager.GetFileBase64(ctx, fileRecord) - if logoErr == nil { - horizontalLogoBase64 = fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data) - } + if input.horizontalLogoFile != nil { + base64Data, mimeType, logoErr := svc.fileManager.GetFileBase64(ctx, input.horizontalLogoFile) + if logoErr == nil { + horizontalLogoBase64 = fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data) } } @@ -2982,7 +3022,7 @@ func generateDocumentPDF( Major: version.Major, Minor: version.Minor, Classification: classification, - Approvers: approverNames, + Approvers: input.approverNames, PublishedAt: version.PublishedAt, CompanyHorizontalLogoBase64: horizontalLogoBase64, Landscape: isLandscape, diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index d9407700c6..0afc9e3fe2 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -1247,8 +1247,12 @@ func (impl *Implm) Run( proboService, l.Named("document-pdf-worker"), worker.WithInterval(30*time.Second), + worker.WithRegisterer(r), + worker.WithTracerProvider(tp), + ) + documentPDFWorkerCtx, stopDocumentPDFWorker := context.WithCancel( + context.WithoutCancel(ctx), ) - documentPDFWorkerCtx, stopDocumentPDFWorker := context.WithCancel(context.Background()) wg.Go( func() { @@ -1258,6 +1262,25 @@ func (impl *Implm) Run( }, ) + documentApprovalQuorumPDFWorker := probo.NewDocumentApprovalQuorumPDFWorker( + proboService, + l.Named("document-approval-quorum-pdf-worker"), + worker.WithInterval(30*time.Second), + worker.WithRegisterer(r), + worker.WithTracerProvider(tp), + ) + documentApprovalQuorumPDFWorkerCtx, stopDocumentApprovalQuorumPDFWorker := context.WithCancel( + context.WithoutCancel(ctx), + ) + + wg.Go( + func() { + if err := documentApprovalQuorumPDFWorker.Run(documentApprovalQuorumPDFWorkerCtx); err != nil { + cancel(fmt.Errorf("document approval quorum pdf worker crashed: %w", err)) + } + }, + ) + documentNotificationInterval := time.Duration(impl.cfg.Notifications.Document.Interval) * time.Second if documentNotificationInterval <= 0 { documentNotificationInterval = 5 * time.Minute @@ -1543,6 +1566,7 @@ func (impl *Implm) Run( stopVettingWorker() stopEvidenceDescriptionWorker() stopDocumentPDFWorker() + stopDocumentApprovalQuorumPDFWorker() stopDocumentNotification() stopExportJobExporter() stopAccessReviewWorker() diff --git a/pkg/server/api/console/v1/base_resolvers.go b/pkg/server/api/console/v1/base_resolvers.go index aaa430c1fc..2fc0d412ba 100644 --- a/pkg/server/api/console/v1/base_resolvers.go +++ b/pkg/server/api/console/v1/base_resolvers.go @@ -266,6 +266,16 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error return types.NewDocumentVersionSignature(documentVersionSignature), nil } + case coredata.DocumentVersionApprovalQuorumEntityType: + action = probo.ActionDocumentVersionApprovalList + loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) { + quorum, err := r.probo.DocumentApprovals.GetQuorum(ctx, scope, id) + if err != nil { + return nil, err + } + + return types.NewDocumentVersionApprovalQuorum(quorum), nil + } case coredata.AssetEntityType: action = probo.ActionAssetList loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {