Add document archiving

Documents can be archived and unarchived. Archived documents are
read-only, excluded from the trust center, and moved to a dedicated
Archived tab in the document list.

- Add archived_at timestamp and status (ACTIVE/ARCHIVED) PG enum column
- Rename DocumentStatus → DocumentVersionStatus, introduce DocumentStatus
- Archive/unarchive mutations in GraphQL, MCP, and CLI
- Bulk archive/unarchive mutations with Active/Archived tabs in the list
- ABAC policies: write actions denied on archived docs, unarchive denied
  on active docs
- Remove control/risk mappings and reset trust center visibility on archive
- Exclude archived documents from mapping dialogs and trust center tab

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-03-19 14:15:16 +01:00
parent 2e12c11c0c
commit 1db8e7133e
29 changed files with 1194 additions and 199 deletions

View File

@@ -113,3 +113,33 @@ WHERE
_, err := conn.Exec(ctx, q, args)
return err
}
func (cp ControlDocument) DeleteByDocumentIDs(
ctx context.Context,
conn pg.Conn,
scope Scoper,
documentIDs []gid.GID,
) error {
q := `
DELETE
FROM
controls_documents
WHERE
%s
AND document_id = ANY(@document_ids);
`
args := pgx.StrictNamedArgs{
"document_ids": documentIDs,
}
maps.Copy(args, scope.SQLArguments())
q = fmt.Sprintf(q, scope.SQLFragment())
if _, err := conn.Exec(ctx, q, args); err != nil {
return fmt.Errorf("cannot delete control document mappings by document ids: %w", err)
}
return nil
}

View File

@@ -37,6 +37,8 @@ type (
Classification DocumentClassification `db:"classification"`
CurrentPublishedVersion *int `db:"current_published_version"`
TrustCenterVisibility TrustCenterVisibility `db:"trust_center_visibility"`
Status DocumentStatus `db:"status"`
ArchivedAt *time.Time `db:"archived_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
@@ -59,17 +61,21 @@ func (p Document) CursorKey(orderBy DocumentOrderField) page.CursorKey {
// AuthorizationAttributes returns the authorization attributes for policy evaluation.
func (d *Document) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) {
q := `SELECT organization_id FROM documents WHERE id = $1 LIMIT 1;`
q := `SELECT organization_id, status FROM documents WHERE id = $1 LIMIT 1;`
var organizationID gid.GID
if err := conn.QueryRow(ctx, q, d.ID).Scan(&organizationID); err != nil {
var documentStatus DocumentStatus
if err := conn.QueryRow(ctx, q, d.ID).Scan(&organizationID, &documentStatus); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrResourceNotFound
}
return nil, fmt.Errorf("cannot query document authorization attributes: %w", err)
}
return map[string]string{"organization_id": organizationID.String()}, nil
return map[string]string{
"organization_id": organizationID.String(),
"document_status": documentStatus.String(),
}, nil
}
func (p *Document) LoadByID(
@@ -87,6 +93,8 @@ SELECT
classification,
current_published_version,
trust_center_visibility,
status,
archived_at,
created_at,
updated_at
FROM
@@ -138,6 +146,8 @@ SELECT
classification,
current_published_version,
trust_center_visibility,
status,
archived_at,
created_at,
updated_at
FROM
@@ -190,6 +200,8 @@ SELECT
classification,
current_published_version,
trust_center_visibility,
status,
archived_at,
created_at,
updated_at
FROM
@@ -271,6 +283,8 @@ SELECT
classification,
current_published_version,
trust_center_visibility,
status,
archived_at,
created_at,
updated_at
FROM
@@ -321,6 +335,8 @@ SELECT
classification,
current_published_version,
trust_center_visibility,
status,
archived_at,
created_at,
updated_at
FROM
@@ -384,6 +400,8 @@ SELECT
classification,
current_published_version,
trust_center_visibility,
status,
archived_at,
created_at,
updated_at
FROM
@@ -431,6 +449,8 @@ INSERT INTO
classification,
current_published_version,
trust_center_visibility,
status,
archived_at,
created_at,
updated_at
)
@@ -443,6 +463,8 @@ VALUES (
@classification,
@current_published_version,
@trust_center_visibility,
@status,
@archived_at,
@created_at,
@updated_at
);
@@ -457,6 +479,8 @@ VALUES (
"classification": p.Classification,
"current_published_version": p.CurrentPublishedVersion,
"trust_center_visibility": p.TrustCenterVisibility,
"status": p.Status,
"archived_at": p.ArchivedAt,
"created_at": p.CreatedAt,
"updated_at": p.UpdatedAt,
}
@@ -515,6 +539,8 @@ SET
document_type = @document_type,
classification = @classification,
trust_center_visibility = @trust_center_visibility,
status = @status,
archived_at = @archived_at,
updated_at = @updated_at
WHERE
%s
@@ -531,6 +557,8 @@ WHERE
"document_type": p.DocumentType,
"classification": p.Classification,
"trust_center_visibility": p.TrustCenterVisibility,
"status": p.Status,
"archived_at": p.ArchivedAt,
}
maps.Copy(args, scope.SQLArguments())
@@ -603,6 +631,8 @@ SELECT
scoped_documents.classification,
scoped_documents.current_published_version,
scoped_documents.trust_center_visibility,
scoped_documents.status,
scoped_documents.archived_at,
scoped_documents.created_at,
scoped_documents.updated_at
FROM scoped_documents
@@ -692,6 +722,8 @@ SELECT
scoped_documents.classification,
scoped_documents.current_published_version,
scoped_documents.trust_center_visibility,
scoped_documents.status,
scoped_documents.archived_at,
scoped_documents.created_at,
scoped_documents.updated_at
FROM scoped_documents
@@ -744,6 +776,59 @@ UPDATE documents SET deleted_at = @deleted_at WHERE %s AND id = ANY(@document_id
return err
}
func (p *Documents) BulkArchive(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
UPDATE documents SET status = 'ARCHIVED', archived_at = @archived_at, trust_center_visibility = 'NONE' WHERE %s AND id = ANY(@document_ids)
`
q = fmt.Sprintf(q, scope.SQLFragment())
ids := make([]gid.GID, len(*p))
for i, doc := range *p {
ids[i] = doc.ID
}
args := pgx.StrictNamedArgs{
"document_ids": ids,
"archived_at": time.Now(),
}
maps.Copy(args, scope.SQLArguments())
if _, err := conn.Exec(ctx, q, args); err != nil {
return fmt.Errorf("cannot bulk archive documents: %w", err)
}
return nil
}
func (p *Documents) BulkUnarchive(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
UPDATE documents SET status = 'ACTIVE', archived_at = NULL WHERE %s AND id = ANY(@document_ids)
`
q = fmt.Sprintf(q, scope.SQLFragment())
ids := make([]gid.GID, len(*p))
for i, doc := range *p {
ids[i] = doc.ID
}
args := pgx.StrictNamedArgs{
"document_ids": ids,
}
maps.Copy(args, scope.SQLArguments())
if _, err := conn.Exec(ctx, q, args); err != nil {
return fmt.Errorf("cannot bulk unarchive documents: %w", err)
}
return nil
}
func (p *Document) IsLastSignableVersionSignedByUserEmail(
ctx context.Context,
conn pg.Conn,

View File

@@ -26,6 +26,7 @@ type (
published *bool
userEmail *mail.Addr
documentTypes []DocumentType
status []DocumentStatus
}
)
@@ -43,6 +44,7 @@ func NewDocumentTrustCenterFilter() *DocumentFilter {
TrustCenterVisibilityPublic,
},
published: &published,
status: []DocumentStatus{DocumentStatusActive},
}
}
@@ -61,6 +63,11 @@ func (f *DocumentFilter) WithDocumentTypes(documentTypes []DocumentType) *Docume
return f
}
func (f *DocumentFilter) WithStatus(status []DocumentStatus) *DocumentFilter {
f.status = status
return f
}
func (f *DocumentFilter) SQLArguments() pgx.NamedArgs {
var visibilities []string
if f.trustCenterVisibilities != nil {
@@ -78,12 +85,21 @@ func (f *DocumentFilter) SQLArguments() pgx.NamedArgs {
}
}
var status []string
if f.status != nil {
status = make([]string, len(f.status))
for i, s := range f.status {
status[i] = s.String()
}
}
return pgx.NamedArgs{
"query": f.query,
"trust_center_visibilities": visibilities,
"published": f.published,
"user_email": f.userEmail,
"document_types": documentTypes,
"document_status": status,
}
}
@@ -131,5 +147,10 @@ func (f *DocumentFilter) SQLFragment() string {
document_type = ANY(@document_types::document_type[])
ELSE TRUE
END
AND
CASE
WHEN @document_status::text[] IS NULL THEN TRUE
ELSE status::text = ANY(@document_status::text[])
END
)`
}

View File

@@ -19,56 +19,43 @@ import (
"fmt"
)
type (
DocumentStatus uint8
)
type DocumentStatus string
const (
DocumentStatusDraft DocumentStatus = iota
DocumentStatusPublished
DocumentStatusActive DocumentStatus = "ACTIVE"
DocumentStatusArchived DocumentStatus = "ARCHIVED"
)
func (ps DocumentStatus) MarshalText() ([]byte, error) {
return []byte(ps.String()), nil
func (s DocumentStatus) IsValid() bool {
switch s {
case DocumentStatusActive, DocumentStatusArchived:
return true
}
return false
}
func (ps *DocumentStatus) UnmarshalText(data []byte) error {
val := string(data)
func (s DocumentStatus) String() string { return string(s) }
switch val {
case DocumentStatusDraft.String():
*ps = DocumentStatusDraft
case DocumentStatusPublished.String():
*ps = DocumentStatusPublished
default:
return fmt.Errorf("invalid DocumentStatus value: %q", val)
func (s *DocumentStatus) UnmarshalText(text []byte) error {
*s = DocumentStatus(text)
if !s.IsValid() {
return fmt.Errorf("%s is not a valid DocumentStatus", string(text))
}
return nil
}
func (ps DocumentStatus) String() string {
var val string
switch ps {
case DocumentStatusDraft:
val = "DRAFT"
case DocumentStatusPublished:
val = "PUBLISHED"
}
return val
func (s DocumentStatus) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}
func (ps *DocumentStatus) Scan(value any) error {
func (s *DocumentStatus) Scan(value any) error {
val, ok := value.(string)
if !ok {
return fmt.Errorf("invalid scan source for DocumentStatus, expected string got %T", value)
}
return ps.UnmarshalText([]byte(val))
return s.UnmarshalText([]byte(val))
}
func (ps DocumentStatus) Value() (driver.Value, error) {
return ps.String(), nil
func (s DocumentStatus) Value() (driver.Value, error) {
return s.String(), nil
}

View File

@@ -38,7 +38,7 @@ type (
Classification DocumentClassification `db:"classification"`
Content string `db:"content"`
Changelog string `db:"changelog"`
Status DocumentStatus `db:"status"`
Status DocumentVersionStatus `db:"status"`
PublishedAt *time.Time `db:"published_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
@@ -49,17 +49,29 @@ type (
// AuthorizationAttributes returns the authorization attributes for policy evaluation.
func (dv *DocumentVersion) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) {
q := `SELECT organization_id FROM document_versions WHERE id = $1 LIMIT 1;`
q := `
SELECT
dv.organization_id,
d.status
FROM document_versions dv
INNER JOIN documents d ON d.id = dv.document_id
WHERE dv.id = $1
LIMIT 1;
`
var organizationID gid.GID
if err := conn.QueryRow(ctx, q, dv.ID).Scan(&organizationID); err != nil {
var documentStatus DocumentStatus
if err := conn.QueryRow(ctx, q, dv.ID).Scan(&organizationID, &documentStatus); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrResourceNotFound
}
return nil, fmt.Errorf("cannot query document version authorization attributes: %w", err)
}
return map[string]string{"organization_id": organizationID.String()}, nil
return map[string]string{
"organization_id": organizationID.String(),
"document_status": documentStatus.String(),
}, nil
}
func (dv *DocumentVersions) LoadByDocumentID(
@@ -377,7 +389,7 @@ LIMIT 1;
args := pgx.StrictNamedArgs{
"document_id": documentID,
"status": DocumentStatusPublished,
"status": DocumentVersionStatusPublished,
}
maps.Copy(args, scope.SQLArguments())

View File

@@ -0,0 +1,74 @@
// 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 (
DocumentVersionStatus uint8
)
const (
DocumentVersionStatusDraft DocumentVersionStatus = iota
DocumentVersionStatusPublished
)
func (ps DocumentVersionStatus) MarshalText() ([]byte, error) {
return []byte(ps.String()), nil
}
func (ps *DocumentVersionStatus) UnmarshalText(data []byte) error {
val := string(data)
switch val {
case DocumentVersionStatusDraft.String():
*ps = DocumentVersionStatusDraft
case DocumentVersionStatusPublished.String():
*ps = DocumentVersionStatusPublished
default:
return fmt.Errorf("invalid DocumentVersionStatus value: %q", val)
}
return nil
}
func (ps DocumentVersionStatus) String() string {
var val string
switch ps {
case DocumentVersionStatusDraft:
val = "DRAFT"
case DocumentVersionStatusPublished:
val = "PUBLISHED"
}
return val
}
func (ps *DocumentVersionStatus) Scan(value any) error {
val, ok := value.(string)
if !ok {
return fmt.Errorf("invalid scan source for DocumentVersionStatus, expected string got %T", value)
}
return ps.UnmarshalText([]byte(val))
}
func (ps DocumentVersionStatus) Value() (driver.Value, error) {
return ps.String(), nil
}

View File

@@ -0,0 +1,7 @@
ALTER TYPE policy_status RENAME TO document_version_status;
CREATE TYPE document_status AS ENUM ('ACTIVE', 'ARCHIVED');
ALTER TABLE documents ADD COLUMN archived_at TIMESTAMP WITH TIME ZONE;
ALTER TABLE documents ADD COLUMN status document_status NOT NULL DEFAULT 'ACTIVE';
ALTER TABLE documents ALTER COLUMN status DROP DEFAULT;

View File

@@ -99,3 +99,33 @@ WHERE
_, err := conn.Exec(ctx, q, args)
return err
}
func (rp RiskDocument) DeleteByDocumentIDs(
ctx context.Context,
conn pg.Conn,
scope Scoper,
documentIDs []gid.GID,
) error {
q := `
DELETE
FROM
risks_documents
WHERE
%s
AND document_id = ANY(@document_ids);
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"document_ids": documentIDs,
}
maps.Copy(args, scope.SQLArguments())
if _, err := conn.Exec(ctx, q, args); err != nil {
return fmt.Errorf("cannot delete risk document mappings by document ids: %w", err)
}
return nil
}

View File

@@ -181,6 +181,8 @@ const (
ActionDocumentUpdate = "core:document:update"
ActionDocumentDelete = "core:document:delete"
ActionDocumentChangelogGenerate = "core:document:generate-changelog"
ActionDocumentArchive = "core:document:archive"
ActionDocumentUnarchive = "core:document:unarchive"
ActionDocumentDraftVersionCreate = "core:document:create-draft-version"
ActionDocumentSendSigningNotifications = "core:document:send-signing-notifications"

View File

@@ -49,6 +49,12 @@ type (
ErrDocumentVersionNotDraft struct {
}
ErrDocumentArchived struct {
}
ErrDocumentNotArchived struct {
}
ErrDocumentVersionSignatureAlreadySigned struct {
}
@@ -164,6 +170,14 @@ func (e ErrDocumentVersionNotDraft) Error() string {
return "cannot update a published document version"
}
func (e ErrDocumentArchived) Error() string {
return "cannot modify an archived document"
}
func (e ErrDocumentNotArchived) Error() string {
return "cannot unarchive a document that is not archived"
}
func (e ErrDocumentVersionSignatureAlreadySigned) Error() string {
return "document version signature already signed"
}
@@ -331,7 +345,7 @@ func (s DocumentService) GenerateChangelog(
return fmt.Errorf("cannot load draft version: %w", err)
}
if draftVersion.Status != coredata.DocumentStatusDraft {
if draftVersion.Status != coredata.DocumentVersionStatusDraft {
return fmt.Errorf("latest version is not a draft")
}
@@ -450,15 +464,19 @@ func (s *DocumentService) publishVersionInTx(
return nil, nil, fmt.Errorf("cannot load document %q: %w", documentID, err)
}
if document.ArchivedAt != nil {
return nil, nil, &ErrDocumentArchived{}
}
if err := documentVersion.LoadLatestVersion(ctx, tx, s.svc.scope, documentID); err != nil {
return nil, nil, fmt.Errorf("cannot load current draft: %w", err)
}
if ignoreExisting && documentVersion.Status == coredata.DocumentStatusPublished {
if ignoreExisting && documentVersion.Status == coredata.DocumentVersionStatusPublished {
return document, documentVersion, nil
}
if documentVersion.Status != coredata.DocumentStatusDraft {
if documentVersion.Status != coredata.DocumentVersionStatusDraft {
return nil, nil, fmt.Errorf("cannot publish version")
}
@@ -479,7 +497,7 @@ func (s *DocumentService) publishVersionInTx(
document.CurrentPublishedVersion = &documentVersion.VersionNumber
document.UpdatedAt = now
documentVersion.Status = coredata.DocumentStatusPublished
documentVersion.Status = coredata.DocumentVersionStatusPublished
documentVersion.PublishedAt = &now
documentVersion.UpdatedAt = now
@@ -514,6 +532,7 @@ func (s *DocumentService) Create(
DocumentType: req.DocumentType,
TrustCenterVisibility: coredata.TrustCenterVisibilityNone,
Classification: req.Classification,
Status: coredata.DocumentStatusActive,
CreatedAt: now,
UpdatedAt: now,
}
@@ -528,7 +547,7 @@ func (s *DocumentService) Create(
Title: req.Title,
VersionNumber: 1,
Content: req.Content,
Status: coredata.DocumentStatusDraft,
Status: coredata.DocumentVersionStatusDraft,
Classification: req.Classification,
CreatedAt: now,
UpdatedAt: now,
@@ -741,7 +760,7 @@ func (s *DocumentService) signDocumentVersionInTx(
return nil, fmt.Errorf("cannot load document version %q: %w", documentVersionID, err)
}
if documentVersion.Status != coredata.DocumentStatusPublished {
if documentVersion.Status != coredata.DocumentVersionStatusPublished {
return nil, fmt.Errorf("cannot sign unpublished version")
}
@@ -790,7 +809,11 @@ func (s *DocumentService) UpdateVersion(
return fmt.Errorf("cannot load document %q: %w", documentVersion.DocumentID, err)
}
if documentVersion.Status != coredata.DocumentStatusDraft {
if document.ArchivedAt != nil {
return &ErrDocumentArchived{}
}
if documentVersion.Status != coredata.DocumentVersionStatusDraft {
return &ErrDocumentVersionNotDraft{}
}
@@ -871,7 +894,7 @@ func (s *DocumentService) BulkRequestSignatures(
return fmt.Errorf("cannot load latest version for document %q: %w", documentID, err)
}
if documentVersion.Status != coredata.DocumentStatusPublished {
if documentVersion.Status != coredata.DocumentVersionStatusPublished {
return fmt.Errorf("cannot request signature for unpublished document %q", documentID)
}
@@ -948,7 +971,7 @@ func (s *DocumentService) RequestSignature(
return nil, fmt.Errorf("cannot get document version: %w", err)
}
if documentVersion.Status != coredata.DocumentStatusPublished {
if documentVersion.Status != coredata.DocumentVersionStatusPublished {
return nil, fmt.Errorf("cannot request signature for unpublished version")
}
@@ -1046,7 +1069,7 @@ func (s *DocumentService) CreateDraft(
return fmt.Errorf("cannot load latest version: %w", err)
}
if latestVersion.Status != coredata.DocumentStatusPublished {
if latestVersion.Status != coredata.DocumentVersionStatusPublished {
return fmt.Errorf("cannot create draft from unpublished version")
}
@@ -1057,7 +1080,7 @@ func (s *DocumentService) CreateDraft(
draftVersion.VersionNumber = latestVersion.VersionNumber + 1
draftVersion.Classification = document.Classification
draftVersion.Content = latestVersion.Content
draftVersion.Status = coredata.DocumentStatusDraft
draftVersion.Status = coredata.DocumentVersionStatusDraft
draftVersion.CreatedAt = now
draftVersion.UpdatedAt = now
@@ -1106,7 +1129,7 @@ func (s *DocumentService) DeleteDraft(
return fmt.Errorf("cannot load document version: %w", err)
}
if documentVersion.Status != coredata.DocumentStatusDraft {
if documentVersion.Status != coredata.DocumentVersionStatusDraft {
return fmt.Errorf("cannot delete published document version")
}
@@ -1155,6 +1178,52 @@ func (s *DocumentService) BulkSoftDelete(
)
}
func (s *DocumentService) BulkArchive(
ctx context.Context,
documentIDs []gid.GID,
) error {
documents := coredata.Documents{}
for _, documentID := range documentIDs {
documents = append(documents, &coredata.Document{ID: documentID})
}
return s.svc.pg.WithTx(
ctx,
func(tx pg.Conn) error {
controlDocument := coredata.ControlDocument{}
if err := controlDocument.DeleteByDocumentIDs(ctx, tx, s.svc.scope, documentIDs); err != nil {
return fmt.Errorf("cannot delete control mappings: %w", err)
}
riskDocument := coredata.RiskDocument{}
if err := riskDocument.DeleteByDocumentIDs(ctx, tx, s.svc.scope, documentIDs); err != nil {
return fmt.Errorf("cannot delete risk mappings: %w", err)
}
return documents.BulkArchive(ctx, tx, s.svc.scope)
},
)
}
func (s *DocumentService) BulkUnarchive(
ctx context.Context,
documentIDs []gid.GID,
) error {
documents := coredata.Documents{}
for _, documentID := range documentIDs {
documents = append(documents, &coredata.Document{ID: documentID})
}
return s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return documents.BulkUnarchive(ctx, conn, s.svc.scope)
},
)
}
func (s *DocumentService) RequestExport(
ctx context.Context,
documentIDs []gid.GID,
@@ -1519,6 +1588,10 @@ func (s *DocumentService) Update(
return fmt.Errorf("cannot load document %q: %w", req.DocumentID, err)
}
if document.ArchivedAt != nil {
return &ErrDocumentArchived{}
}
if req.Title != nil {
document.Title = *req.Title
}
@@ -1574,7 +1647,7 @@ func (s *DocumentService) Update(
draftVersion := &coredata.DocumentVersion{}
err := draftVersion.LoadLatestVersion(ctx, tx, s.svc.scope, req.DocumentID)
if err == nil && draftVersion.Status == coredata.DocumentStatusDraft {
if err == nil && draftVersion.Status == coredata.DocumentVersionStatusDraft {
draftVersion.Title = document.Title
draftVersion.Classification = document.Classification
draftVersion.UpdatedAt = now
@@ -1614,6 +1687,91 @@ func (s *DocumentService) Update(
return document, nil
}
func (s *DocumentService) Archive(
ctx context.Context,
documentID gid.GID,
) (*coredata.Document, error) {
document := &coredata.Document{}
now := time.Now()
err := s.svc.pg.WithTx(
ctx,
func(tx pg.Conn) error {
if err := document.LoadByID(ctx, tx, s.svc.scope, documentID); err != nil {
return fmt.Errorf("cannot load document %q: %w", documentID, err)
}
if document.ArchivedAt != nil {
return &ErrDocumentArchived{}
}
controlDocument := coredata.ControlDocument{}
if err := controlDocument.DeleteByDocumentIDs(ctx, tx, s.svc.scope, []gid.GID{documentID}); err != nil {
return fmt.Errorf("cannot delete control mappings: %w", err)
}
riskDocument := coredata.RiskDocument{}
if err := riskDocument.DeleteByDocumentIDs(ctx, tx, s.svc.scope, []gid.GID{documentID}); err != nil {
return fmt.Errorf("cannot delete risk mappings: %w", err)
}
document.Status = coredata.DocumentStatusArchived
document.ArchivedAt = &now
document.UpdatedAt = now
document.TrustCenterVisibility = coredata.TrustCenterVisibilityNone
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
return fmt.Errorf("cannot archive document: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return document, nil
}
func (s *DocumentService) Unarchive(
ctx context.Context,
documentID gid.GID,
) (*coredata.Document, error) {
document := &coredata.Document{}
now := time.Now()
err := s.svc.pg.WithTx(
ctx,
func(tx pg.Conn) error {
if err := document.LoadByID(ctx, tx, s.svc.scope, documentID); err != nil {
return fmt.Errorf("cannot load document %q: %w", documentID, err)
}
if document.ArchivedAt == nil {
return &ErrDocumentNotArchived{}
}
document.Status = coredata.DocumentStatusActive
document.ArchivedAt = nil
document.UpdatedAt = now
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
return fmt.Errorf("cannot unarchive document: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return document, nil
}
func (s *DocumentService) CancelSignatureRequest(
ctx context.Context,
documentVersionSignatureID gid.GID,

View File

@@ -19,12 +19,37 @@ import (
"go.probo.inc/probo/pkg/iam/policy"
)
var organizationCondition = policy.Equals("principal.organization_id", "resource.organization_id")
var (
organizationCondition = policy.Equals("principal.organization_id", "resource.organization_id")
documentWriteActiveOnly = policy.Deny(
ActionDocumentUpdate,
ActionDocumentArchive,
ActionDocumentDraftVersionCreate,
ActionDocumentChangelogGenerate,
ActionDocumentSendSigningNotifications,
ActionDocumentVersionUpdate,
ActionDocumentVersionPublish,
ActionDocumentVersionDeleteDraft,
ActionDocumentVersionSignatureRequest,
ActionDocumentVersionCancelSignature,
).WithSID("document-write-active-only").When(
organizationCondition,
policy.Equals("resource.document_status", "ARCHIVED"),
)
documentUnarchiveArchivedOnly = policy.Deny(
ActionDocumentUnarchive,
).WithSID("document-unarchive-archived-only").When(
organizationCondition,
policy.Equals("resource.document_status", "ACTIVE"),
)
)
// OwnerPolicy defines permissions for organization owners.
var OwnerPolicy = policy.NewPolicy(
"probo:owner",
"Probo Owner",
documentWriteActiveOnly,
documentUnarchiveArchivedOnly,
policy.Allow("core:*").WithSID("full-core-access").When(organizationCondition),
).WithDescription("Full probo access for organization owners")
@@ -32,6 +57,8 @@ var OwnerPolicy = policy.NewPolicy(
var AdminPolicy = policy.NewPolicy(
"probo:admin",
"Probo Admin",
documentWriteActiveOnly,
documentUnarchiveArchivedOnly,
policy.Allow("core:*").WithSID("full-core-access").When(organizationCondition),
).WithDescription("Probo admin access - can manage core entities")

View File

@@ -69,15 +69,26 @@ enum EvidenceState
@goEnum(value: "go.probo.inc/probo/pkg/coredata.EvidenceStateRequested")
}
enum DocumentStatus
@goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentStatus") {
DRAFT @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentStatusDraft")
enum DocumentVersionStatus
@goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentVersionStatus") {
DRAFT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DocumentVersionStatusDraft"
)
PUBLISHED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DocumentStatusPublished"
value: "go.probo.inc/probo/pkg/coredata.DocumentVersionStatusPublished"
)
}
enum DocumentStatus
@goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentStatus") {
ACTIVE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentStatusActive")
ARCHIVED
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentStatusArchived")
}
enum EvidenceType
@goModel(model: "go.probo.inc/probo/pkg/coredata.EvidenceType") {
FILE @goEnum(value: "go.probo.inc/probo/pkg/coredata.EvidenceTypeFile")
@@ -1569,7 +1580,7 @@ input ApplicabilityStatementOrder
}
input DocumentVersionFilter {
status: DocumentStatus
status: DocumentVersionStatus
}
# Input Types for Filtering
@@ -1580,6 +1591,7 @@ input ControlFilter {
input DocumentFilter {
query: String
documentTypes: [DocumentType!]
status: [DocumentStatus!]
}
input MeasureFilter {
@@ -2402,6 +2414,9 @@ type Document implements Node {
filter: ControlFilter
): ControlConnection! @goField(forceResolver: true)
status: DocumentStatus!
archivedAt: Datetime
createdAt: Datetime!
updatedAt: Datetime!
@@ -3657,6 +3672,8 @@ type Mutation {
# Document mutations
createDocument(input: CreateDocumentInput!): CreateDocumentPayload!
updateDocument(input: UpdateDocumentInput!): UpdateDocumentPayload!
archiveDocument(input: ArchiveDocumentInput!): ArchiveDocumentPayload!
unarchiveDocument(input: UnarchiveDocumentInput!): UnarchiveDocumentPayload!
deleteDocument(input: DeleteDocumentInput!): DeleteDocumentPayload!
# Meeting mutations
createMeeting(input: CreateMeetingInput!): CreateMeetingPayload!
@@ -3694,6 +3711,12 @@ type Mutation {
bulkDeleteDocuments(
input: BulkDeleteDocumentsInput!
): BulkDeleteDocumentsPayload!
bulkArchiveDocuments(
input: BulkArchiveDocumentsInput!
): BulkArchiveDocumentsPayload!
bulkUnarchiveDocuments(
input: BulkUnarchiveDocumentsInput!
): BulkUnarchiveDocumentsPayload!
bulkExportDocuments(
input: BulkExportDocumentsInput!
): BulkExportDocumentsPayload!
@@ -4367,6 +4390,14 @@ input ExportTransferImpactAssessmentsPDFInput {
filter: TransferImpactAssessmentFilter
}
input ArchiveDocumentInput {
documentId: ID!
}
input UnarchiveDocumentInput {
documentId: ID!
}
input DeleteDocumentInput {
documentId: ID!
}
@@ -5086,6 +5117,14 @@ type UpdateDocumentPayload {
document: Document!
}
type ArchiveDocumentPayload {
document: Document!
}
type UnarchiveDocumentPayload {
document: Document!
}
type DeleteDocumentPayload {
deletedDocumentId: ID!
}
@@ -5197,7 +5236,7 @@ type DeleteMeasurePayload {
type DocumentVersion implements Node {
id: ID!
document: Document! @goField(forceResolver: true)
status: DocumentStatus!
status: DocumentVersionStatus!
version: Int!
content: String!
changelog: String!
@@ -5326,6 +5365,22 @@ input BulkDeleteDocumentsInput {
documentIds: [ID!]!
}
input BulkArchiveDocumentsInput {
documentIds: [ID!]!
}
type BulkArchiveDocumentsPayload {
documents: [Document!]!
}
input BulkUnarchiveDocumentsInput {
documentIds: [ID!]!
}
type BulkUnarchiveDocumentsPayload {
documents: [Document!]!
}
input BulkExportDocumentsInput {
documentIds: [ID!]!
withWatermark: Boolean!

View File

@@ -84,6 +84,8 @@ func NewDocument(document *coredata.Document) *Document {
Classification: document.Classification,
CurrentPublishedVersion: document.CurrentPublishedVersion,
TrustCenterVisibility: document.TrustCenterVisibility,
Status: document.Status,
ArchivedAt: document.ArchivedAt,
CreatedAt: document.CreatedAt,
UpdatedAt: document.UpdatedAt,
}

View File

@@ -4286,6 +4286,9 @@ func (r *mutationResolver) UpdateDocument(ctx context.Context, input types.Updat
)
if err != nil {
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errArchived)
}
if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok {
return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors)
}
@@ -4298,6 +4301,50 @@ func (r *mutationResolver) UpdateDocument(ctx context.Context, input types.Updat
}, nil
}
// ArchiveDocument is the resolver for the archiveDocument field.
func (r *mutationResolver) ArchiveDocument(ctx context.Context, input types.ArchiveDocumentInput) (*types.ArchiveDocumentPayload, error) {
if err := r.authorize(ctx, input.DocumentID, probo.ActionDocumentArchive); err != nil {
return nil, err
}
prb := r.ProboService(ctx, input.DocumentID.TenantID())
document, err := prb.Documents.Archive(ctx, input.DocumentID)
if err != nil {
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errArchived)
}
r.logger.ErrorCtx(ctx, "cannot archive document", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.ArchiveDocumentPayload{
Document: types.NewDocument(document),
}, nil
}
// UnarchiveDocument is the resolver for the unarchiveDocument field.
func (r *mutationResolver) UnarchiveDocument(ctx context.Context, input types.UnarchiveDocumentInput) (*types.UnarchiveDocumentPayload, error) {
if err := r.authorize(ctx, input.DocumentID, probo.ActionDocumentUnarchive); err != nil {
return nil, err
}
prb := r.ProboService(ctx, input.DocumentID.TenantID())
document, err := prb.Documents.Unarchive(ctx, input.DocumentID)
if err != nil {
if errNotArchived, ok := errors.AsType[*probo.ErrDocumentNotArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errNotArchived)
}
r.logger.ErrorCtx(ctx, "cannot unarchive document", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.UnarchiveDocumentPayload{
Document: types.NewDocument(document),
}, nil
}
// DeleteDocument is the resolver for the deleteDocument field.
func (r *mutationResolver) DeleteDocument(ctx context.Context, input types.DeleteDocumentInput) (*types.DeleteDocumentPayload, error) {
if err := r.authorize(ctx, input.DocumentID, probo.ActionDocumentDelete); err != nil {
@@ -4607,6 +4654,10 @@ func (r *mutationResolver) PublishDocumentVersion(ctx context.Context, input typ
return nil, gqlutils.Invalid(ctx, errNoChanges)
}
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errArchived)
}
r.logger.ErrorCtx(ctx, "cannot publish document version", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
@@ -4687,6 +4738,58 @@ func (r *mutationResolver) BulkDeleteDocuments(ctx context.Context, input types.
}, nil
}
// BulkArchiveDocuments is the resolver for the bulkArchiveDocuments field.
func (r *mutationResolver) BulkArchiveDocuments(ctx context.Context, input types.BulkArchiveDocumentsInput) (*types.BulkArchiveDocumentsPayload, error) {
if len(input.DocumentIds) == 0 {
return &types.BulkArchiveDocumentsPayload{
Documents: []*types.Document{},
}, nil
}
for _, documentID := range input.DocumentIds {
if err := r.authorize(ctx, documentID, probo.ActionDocumentArchive); err != nil {
return nil, err
}
}
prb := r.ProboService(ctx, input.DocumentIds[0].TenantID())
if err := prb.Documents.BulkArchive(ctx, input.DocumentIds); err != nil {
r.logger.ErrorCtx(ctx, "cannot bulk archive documents", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.BulkArchiveDocumentsPayload{
Documents: []*types.Document{},
}, nil
}
// BulkUnarchiveDocuments is the resolver for the bulkUnarchiveDocuments field.
func (r *mutationResolver) BulkUnarchiveDocuments(ctx context.Context, input types.BulkUnarchiveDocumentsInput) (*types.BulkUnarchiveDocumentsPayload, error) {
if len(input.DocumentIds) == 0 {
return &types.BulkUnarchiveDocumentsPayload{
Documents: []*types.Document{},
}, nil
}
for _, documentID := range input.DocumentIds {
if err := r.authorize(ctx, documentID, probo.ActionDocumentUnarchive); err != nil {
return nil, err
}
}
prb := r.ProboService(ctx, input.DocumentIds[0].TenantID())
if err := prb.Documents.BulkUnarchive(ctx, input.DocumentIds); err != nil {
r.logger.ErrorCtx(ctx, "cannot bulk unarchive documents", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.BulkUnarchiveDocumentsPayload{
Documents: []*types.Document{},
}, nil
}
// BulkExportDocuments is the resolver for the bulkExportDocuments field.
func (r *mutationResolver) BulkExportDocuments(ctx context.Context, input types.BulkExportDocumentsInput) (*types.BulkExportDocumentsPayload, error) {
if len(input.DocumentIds) == 0 {
@@ -4803,6 +4906,9 @@ func (r *mutationResolver) UpdateDocumentVersion(ctx context.Context, input type
return nil, gqlutils.Conflict(ctx, errNotDraft)
}
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errArchived)
}
if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok {
return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors)
}
@@ -6479,7 +6585,8 @@ func (r *organizationResolver) Documents(ctx context.Context, obj *types.Organiz
var documentFilter = coredata.NewDocumentFilter(nil)
if filter != nil {
documentFilter = coredata.NewDocumentFilter(filter.Query).
WithDocumentTypes(filter.DocumentTypes)
WithDocumentTypes(filter.DocumentTypes).
WithStatus(filter.Status)
}
page, err := prb.Documents.ListByOrganizationID(ctx, obj.ID, cursor, documentFilter)

View File

@@ -799,7 +799,7 @@ func (r *Resolver) AddFindingTool(ctx context.Context, req *mcp.CallToolRequest,
Kind: input.Kind,
Description: input.Description,
Source: input.Source,
IdentifiedOn: input.IdentifiedOn,
IdentifiedOn: input.IdentifiedOn,
RootCause: input.RootCause,
CorrectiveAction: input.CorrectiveAction,
OwnerID: input.OwnerID,
@@ -830,7 +830,7 @@ func (r *Resolver) UpdateFindingTool(ctx context.Context, req *mcp.CallToolReque
ID: input.ID,
Description: UnwrapOmittable(input.Description),
Source: UnwrapOmittable(input.Source),
IdentifiedOn: UnwrapOmittable(input.IdentifiedOn),
IdentifiedOn: UnwrapOmittable(input.IdentifiedOn),
RootCause: UnwrapOmittable(input.RootCause),
CorrectiveAction: UnwrapOmittable(input.CorrectiveAction),
OwnerID: input.OwnerID,
@@ -3201,3 +3201,43 @@ func (r *Resolver) ListFindingAuditsTool(ctx context.Context, req *mcp.CallToolR
return nil, types.NewListFindingAuditsOutput(auditPage), nil
}
func (r *Resolver) ArchiveDocumentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ArchiveDocumentInput) (*mcp.CallToolResult, types.ArchiveDocumentOutput, error) {
r.MustAuthorize(ctx, input.ID, probo.ActionDocumentArchive)
svc := r.ProboService(ctx, input.ID)
document, err := svc.Documents.Archive(ctx, input.ID)
if err != nil {
return nil, types.ArchiveDocumentOutput{}, fmt.Errorf("cannot archive document: %w", err)
}
approverPage, err := svc.Documents.ListApprovers(ctx, input.ID, allApproversCursor())
if err != nil {
return nil, types.ArchiveDocumentOutput{}, fmt.Errorf("cannot list document approvers: %w", err)
}
return nil, types.ArchiveDocumentOutput{
Document: types.NewDocument(document, profileIDs(approverPage)),
}, nil
}
func (r *Resolver) UnarchiveDocumentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UnarchiveDocumentInput) (*mcp.CallToolResult, types.UnarchiveDocumentOutput, error) {
r.MustAuthorize(ctx, input.ID, probo.ActionDocumentUnarchive)
svc := r.ProboService(ctx, input.ID)
document, err := svc.Documents.Unarchive(ctx, input.ID)
if err != nil {
return nil, types.UnarchiveDocumentOutput{}, fmt.Errorf("cannot unarchive document: %w", err)
}
approverPage, err := svc.Documents.ListApprovers(ctx, input.ID, allApproversCursor())
if err != nil {
return nil, types.UnarchiveDocumentOutput{}, fmt.Errorf("cannot list document approvers: %w", err)
}
return nil, types.UnarchiveDocumentOutput{
Document: types.NewDocument(document, profileIDs(approverPage)),
}, nil
}

View File

@@ -5057,11 +5057,18 @@ components:
- SECRET
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.DocumentClassification
DocumentStatus:
DocumentVersionStatus:
type: string
enum:
- DRAFT
- PUBLISHED
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.DocumentVersionStatus
DocumentStatus:
type: string
enum:
- ACTIVE
- ARCHIVED
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.DocumentStatus
DocumentVersionSignatureState:
@@ -5142,6 +5149,7 @@ components:
- document_type
- classification
- trust_center_visibility
- status
- created_at
- updated_at
properties:
@@ -5173,6 +5181,15 @@ components:
trust_center_visibility:
$ref: "#/components/schemas/TrustCenterVisibility"
description: Trust center visibility
status:
$ref: "#/components/schemas/DocumentStatus"
description: Document status
archived_at:
type:
- string
- "null"
format: date-time
description: Archive timestamp
created_at:
type: string
format: date-time
@@ -5228,8 +5245,8 @@ components:
type: string
description: Changelog
status:
$ref: "#/components/schemas/DocumentStatus"
description: Document status
$ref: "#/components/schemas/DocumentVersionStatus"
description: Document version status
published_at:
type:
- string
@@ -5434,6 +5451,40 @@ components:
document:
$ref: "#/components/schemas/Document"
ArchiveDocumentInput:
type: object
required:
- id
properties:
id:
$ref: "#/components/schemas/GID"
description: Document ID
ArchiveDocumentOutput:
type: object
required:
- document
properties:
document:
$ref: "#/components/schemas/Document"
UnarchiveDocumentInput:
type: object
required:
- id
properties:
id:
$ref: "#/components/schemas/GID"
description: Document ID
UnarchiveDocumentOutput:
type: object
required:
- document
properties:
document:
$ref: "#/components/schemas/Document"
ListDocumentVersionsInput:
type: object
required:
@@ -7133,6 +7184,22 @@ tools:
$ref: "#/components/schemas/UpdateDocumentInput"
outputSchema:
$ref: "#/components/schemas/UpdateDocumentOutput"
- name: archiveDocument
description: Archive a document to prevent further modifications
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/ArchiveDocumentInput"
outputSchema:
$ref: "#/components/schemas/ArchiveDocumentOutput"
- name: unarchiveDocument
description: Unarchive a document to allow modifications again
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/UnarchiveDocumentInput"
outputSchema:
$ref: "#/components/schemas/UnarchiveDocumentOutput"
- name: listDocumentVersions
description: List all versions for a document
hints:

View File

@@ -30,6 +30,8 @@ func NewDocument(d *coredata.Document, approverIDs []gid.GID) *Document {
Classification: d.Classification,
CurrentPublishedVersion: d.CurrentPublishedVersion,
TrustCenterVisibility: d.TrustCenterVisibility,
Status: d.Status,
ArchivedAt: d.ArchivedAt,
CreatedAt: d.CreatedAt,
UpdatedAt: d.UpdatedAt,
}

View File

@@ -95,6 +95,12 @@ func (r *documentResolver) IsUserAuthorized(ctx context.Context, obj *types.Docu
document, err := trustService.Documents.Get(ctx, trustCenter.OrganizationID, obj.ID)
if err != nil {
if errors.Is(err, trust.ErrDocumentNotFound) || errors.Is(err, trust.ErrDocumentNotVisible) || errors.Is(err, coredata.ErrResourceNotFound) {
return false, gqlutils.NotFoundf(ctx, "document %q not found", obj.ID)
}
if _, ok := errors.AsType[*trust.ErrDocumentArchived](err); ok {
return false, gqlutils.NotFoundf(ctx, "document %q not found", obj.ID)
}
r.logger.ErrorCtx(ctx, "cannot load document", log.Error(err))
return false, gqlutils.Internal(ctx)
}
@@ -363,6 +369,12 @@ func (r *mutationResolver) ExportDocumentPDF(ctx context.Context, input types.Ex
document, err := trustService.Documents.Get(ctx, trustCenter.OrganizationID, input.DocumentID)
if err != nil {
if errors.Is(err, trust.ErrDocumentNotFound) || errors.Is(err, trust.ErrDocumentNotVisible) || errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFoundf(ctx, "document %q not found", input.DocumentID)
}
if _, ok := errors.AsType[*trust.ErrDocumentArchived](err); ok {
return nil, gqlutils.NotFoundf(ctx, "document %q not found", input.DocumentID)
}
r.logger.ErrorCtx(ctx, "cannot load document", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
@@ -525,6 +537,12 @@ func (r *mutationResolver) RequestDocumentAccess(ctx context.Context, input type
document, err := trustService.Documents.Get(ctx, trustCenter.OrganizationID, input.DocumentID)
if err != nil {
if errors.Is(err, trust.ErrDocumentNotFound) || errors.Is(err, trust.ErrDocumentNotVisible) || errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFoundf(ctx, "document %q not found", input.DocumentID)
}
if _, ok := errors.AsType[*trust.ErrDocumentArchived](err); ok {
return nil, gqlutils.NotFoundf(ctx, "document %q not found", input.DocumentID)
}
r.logger.ErrorCtx(ctx, "cannot load document", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
@@ -871,6 +889,9 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
if errors.Is(err, trust.ErrDocumentNotFound) || errors.Is(err, trust.ErrDocumentNotVisible) || errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFoundf(ctx, "node %q not found", id)
}
if _, ok := errors.AsType[*trust.ErrDocumentArchived](err); ok {
return nil, gqlutils.NotFoundf(ctx, "node %q not found", id)
}
r.logger.ErrorCtx(ctx, "cannot get document", log.Error(err))
return nil, gqlutils.Internal(ctx)
}

View File

@@ -34,8 +34,14 @@ type (
svc *TenantService
html2pdfConverter *html2pdf.Converter
}
ErrDocumentArchived struct{}
)
func (e ErrDocumentArchived) Error() string {
return "cannot access an archived document"
}
func (s *DocumentService) ListForOrganizationId(
ctx context.Context,
organizationID gid.GID,
@@ -103,6 +109,10 @@ func (s DocumentService) Get(
return fmt.Errorf("cannot load document: %w", err)
}
if document.ArchivedAt != nil {
return &ErrDocumentArchived{}
}
return nil
},
)
@@ -138,6 +148,10 @@ func (s *DocumentService) exportPDFData(
return fmt.Errorf("cannot load document: %w", err)
}
if document.ArchivedAt != nil {
return &ErrDocumentArchived{}
}
if document.TrustCenterVisibility == coredata.TrustCenterVisibilityNone {
return fmt.Errorf("document not visible on trust center")
}