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:
@@ -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!
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user