Add major.minor document versioning
Introduce a two-part version scheme (major.minor) for documents. Drafts start at 0.1 and increment minor on each new draft. Publishing as minor keeps the current version, publishing as major bumps to the next major.0. Both current_published_major and current_published_minor are tracked on the document for exact version lookups. Signatures and approval quorums aggregate across all versions sharing the same major number using CTE joins. Approval page mutations spread the decision fragment so Relay updates the version row state without requiring a page refresh. GraphQL, MCP, and service layer expose separate publishMajor and publishMinor mutations instead of a single mutation with a type enum. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -2441,7 +2441,8 @@ type Document implements Node {
|
||||
description: String
|
||||
documentType: DocumentType!
|
||||
classification: DocumentClassification!
|
||||
currentPublishedVersion: Int
|
||||
currentPublishedMajor: Int
|
||||
currentPublishedMinor: Int
|
||||
trustCenterVisibility: TrustCenterVisibility!
|
||||
organization: Organization! @goField(forceResolver: true)
|
||||
|
||||
@@ -2501,7 +2502,8 @@ type EmployeeDocumentVersion
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.EmployeeDocumentVersion"
|
||||
) {
|
||||
id: ID!
|
||||
version: Int!
|
||||
major: Int!
|
||||
minor: Int!
|
||||
status: DocumentVersionStatus!
|
||||
signed: Boolean! @goField(forceResolver: true)
|
||||
approvalDecision: DocumentVersionApprovalDecision @goField(forceResolver: true)
|
||||
@@ -3792,10 +3794,16 @@ type Mutation {
|
||||
exportStateOfApplicabilityPDF(
|
||||
input: ExportStateOfApplicabilityPDFInput!
|
||||
): ExportStateOfApplicabilityPDFPayload!
|
||||
publishDocumentVersion(
|
||||
input: PublishDocumentVersionInput!
|
||||
publishMajorDocumentVersion(
|
||||
input: PublishMajorDocumentVersionInput!
|
||||
): PublishDocumentVersionPayload!
|
||||
bulkPublishDocumentVersions(
|
||||
publishMinorDocumentVersion(
|
||||
input: PublishMinorDocumentVersionInput!
|
||||
): PublishDocumentVersionPayload!
|
||||
bulkPublishMajorDocumentVersions(
|
||||
input: BulkPublishDocumentVersionsInput!
|
||||
): BulkPublishDocumentVersionsPayload!
|
||||
bulkPublishMinorDocumentVersions(
|
||||
input: BulkPublishDocumentVersionsInput!
|
||||
): BulkPublishDocumentVersionsPayload!
|
||||
requestDocumentVersionApproval(
|
||||
@@ -5355,7 +5363,8 @@ type DocumentVersion implements Node {
|
||||
id: ID!
|
||||
document: Document! @goField(forceResolver: true)
|
||||
status: DocumentVersionStatus!
|
||||
version: Int!
|
||||
major: Int!
|
||||
minor: Int!
|
||||
content: String!
|
||||
changelog: String!
|
||||
title: String!
|
||||
@@ -5686,7 +5695,12 @@ type RequestDocumentVersionApprovalPayload {
|
||||
approvalQuorum: DocumentVersionApprovalQuorum!
|
||||
}
|
||||
|
||||
input PublishDocumentVersionInput {
|
||||
input PublishMajorDocumentVersionInput {
|
||||
documentId: ID!
|
||||
changelog: String
|
||||
}
|
||||
|
||||
input PublishMinorDocumentVersionInput {
|
||||
documentId: ID!
|
||||
changelog: String
|
||||
}
|
||||
|
||||
@@ -80,13 +80,14 @@ func NewDocument(document *coredata.Document) *Document {
|
||||
Organization: &Organization{
|
||||
ID: document.OrganizationID,
|
||||
},
|
||||
DocumentType: document.DocumentType,
|
||||
Classification: document.Classification,
|
||||
CurrentPublishedVersion: document.CurrentPublishedVersion,
|
||||
TrustCenterVisibility: document.TrustCenterVisibility,
|
||||
Status: document.Status,
|
||||
ArchivedAt: document.ArchivedAt,
|
||||
CreatedAt: document.CreatedAt,
|
||||
UpdatedAt: document.UpdatedAt,
|
||||
DocumentType: document.DocumentType,
|
||||
Classification: document.Classification,
|
||||
CurrentPublishedMajor: document.CurrentPublishedMajor,
|
||||
CurrentPublishedMinor: document.CurrentPublishedMinor,
|
||||
TrustCenterVisibility: document.TrustCenterVisibility,
|
||||
Status: document.Status,
|
||||
ArchivedAt: document.ArchivedAt,
|
||||
CreatedAt: document.CreatedAt,
|
||||
UpdatedAt: document.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,8 @@ func NewDocumentVersion(documentVersion *coredata.DocumentVersion) *DocumentVers
|
||||
Document: &Document{
|
||||
ID: documentVersion.DocumentID,
|
||||
},
|
||||
Version: documentVersion.VersionNumber,
|
||||
Major: documentVersion.Major,
|
||||
Minor: documentVersion.Minor,
|
||||
Title: documentVersion.Title,
|
||||
Content: documentVersion.Content,
|
||||
Status: documentVersion.Status,
|
||||
|
||||
@@ -66,7 +66,8 @@ type (
|
||||
EmployeeDocumentVersion struct {
|
||||
ID gid.GID
|
||||
OrganizationID gid.GID
|
||||
Version int
|
||||
Major int
|
||||
Minor int
|
||||
Status coredata.DocumentVersionStatus
|
||||
PublishedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
|
||||
@@ -1647,7 +1647,8 @@ func (r *employeeDocumentResolver) Versions(ctx context.Context, obj *types.Empl
|
||||
employeeVersions[i] = &types.EmployeeDocumentVersion{
|
||||
ID: v.ID,
|
||||
OrganizationID: v.OrganizationID,
|
||||
Version: v.VersionNumber,
|
||||
Major: v.Major,
|
||||
Minor: v.Minor,
|
||||
Status: v.Status,
|
||||
PublishedAt: v.PublishedAt,
|
||||
CreatedAt: v.CreatedAt,
|
||||
@@ -5012,16 +5013,20 @@ func (r *mutationResolver) ExportStateOfApplicabilityPDF(ctx context.Context, in
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PublishDocumentVersion is the resolver for the publishDocumentVersion field.
|
||||
func (r *mutationResolver) PublishDocumentVersion(ctx context.Context, input types.PublishDocumentVersionInput) (*types.PublishDocumentVersionPayload, error) {
|
||||
// PublishMajorDocumentVersion is the resolver for the publishMajorDocumentVersion field.
|
||||
func (r *mutationResolver) PublishMajorDocumentVersion(ctx context.Context, input types.PublishMajorDocumentVersionInput) (*types.PublishDocumentVersionPayload, error) {
|
||||
if err := r.authorize(ctx, input.DocumentID, probo.ActionDocumentVersionPublish); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
identity := authn.IdentityFromContext(ctx)
|
||||
prb := r.ProboService(ctx, input.DocumentID.TenantID())
|
||||
|
||||
document, documentVersion, err := prb.Documents.PublishVersion(ctx, input.DocumentID, identity.ID, input.Changelog)
|
||||
document, documentVersion, err := prb.Documents.PublishMajorVersion(
|
||||
ctx,
|
||||
input.DocumentID,
|
||||
authn.IdentityFromContext(ctx).ID,
|
||||
input.Changelog,
|
||||
)
|
||||
if err != nil {
|
||||
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
|
||||
return nil, gqlutils.Conflict(ctx, errArchived)
|
||||
@@ -5031,7 +5036,11 @@ func (r *mutationResolver) PublishDocumentVersion(ctx context.Context, input typ
|
||||
return nil, gqlutils.Invalid(ctx, errNotDraft)
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot publish document version", log.Error(err))
|
||||
if errNoChanges, ok := errors.AsType[*probo.ErrDocumentVersionNoChanges](err); ok {
|
||||
return nil, gqlutils.Invalid(ctx, errNoChanges)
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot publish major document version", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
@@ -5041,8 +5050,41 @@ func (r *mutationResolver) PublishDocumentVersion(ctx context.Context, input typ
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BulkPublishDocumentVersions is the resolver for the bulkPublishDocumentVersions field.
|
||||
func (r *mutationResolver) BulkPublishDocumentVersions(ctx context.Context, input types.BulkPublishDocumentVersionsInput) (*types.BulkPublishDocumentVersionsPayload, error) {
|
||||
// PublishMinorDocumentVersion is the resolver for the publishMinorDocumentVersion field.
|
||||
func (r *mutationResolver) PublishMinorDocumentVersion(ctx context.Context, input types.PublishMinorDocumentVersionInput) (*types.PublishDocumentVersionPayload, error) {
|
||||
if err := r.authorize(ctx, input.DocumentID, probo.ActionDocumentVersionPublish); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prb := r.ProboService(ctx, input.DocumentID.TenantID())
|
||||
|
||||
document, documentVersion, err := prb.Documents.PublishMinorVersion(
|
||||
ctx,
|
||||
input.DocumentID,
|
||||
authn.IdentityFromContext(ctx).ID,
|
||||
input.Changelog,
|
||||
)
|
||||
if err != nil {
|
||||
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
|
||||
return nil, gqlutils.Conflict(ctx, errArchived)
|
||||
}
|
||||
|
||||
if errNotDraft, ok := errors.AsType[*probo.ErrDocumentVersionNotDraft](err); ok {
|
||||
return nil, gqlutils.Invalid(ctx, errNotDraft)
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot publish minor document version", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return &types.PublishDocumentVersionPayload{
|
||||
Document: types.NewDocument(document),
|
||||
DocumentVersion: types.NewDocumentVersion(documentVersion),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BulkPublishMajorDocumentVersions is the resolver for the bulkPublishMajorDocumentVersions field.
|
||||
func (r *mutationResolver) BulkPublishMajorDocumentVersions(ctx context.Context, input types.BulkPublishDocumentVersionsInput) (*types.BulkPublishDocumentVersionsPayload, error) {
|
||||
if len(input.DocumentIds) == 0 {
|
||||
return &types.BulkPublishDocumentVersionsPayload{
|
||||
DocumentVersions: []*types.DocumentVersion{},
|
||||
@@ -5056,12 +5098,10 @@ func (r *mutationResolver) BulkPublishDocumentVersions(ctx context.Context, inpu
|
||||
}
|
||||
}
|
||||
|
||||
identity := authn.IdentityFromContext(ctx)
|
||||
prb := r.ProboService(ctx, input.DocumentIds[0].TenantID())
|
||||
|
||||
versions, documents, err := prb.Documents.BulkPublishVersions(ctx, probo.BulkPublishVersionsRequest{
|
||||
versions, documents, err := prb.Documents.BulkPublishMajorVersions(ctx, probo.BulkPublishVersionsRequest{
|
||||
DocumentIDs: input.DocumentIds,
|
||||
PublishedBy: identity.ID,
|
||||
Changelog: input.Changelog,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -5073,7 +5113,57 @@ func (r *mutationResolver) BulkPublishDocumentVersions(ctx context.Context, inpu
|
||||
return nil, gqlutils.Invalid(ctx, errNotDraft)
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot bulk publish document versions", log.Error(err))
|
||||
r.logger.ErrorCtx(ctx, "cannot bulk publish major document versions", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
typesVersions := make([]*types.DocumentVersion, len(versions))
|
||||
for i, v := range versions {
|
||||
typesVersions[i] = types.NewDocumentVersion(v)
|
||||
}
|
||||
|
||||
typesDocuments := make([]*types.Document, len(documents))
|
||||
for i, d := range documents {
|
||||
typesDocuments[i] = types.NewDocument(d)
|
||||
}
|
||||
|
||||
return &types.BulkPublishDocumentVersionsPayload{
|
||||
DocumentVersions: typesVersions,
|
||||
Documents: typesDocuments,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BulkPublishMinorDocumentVersions is the resolver for the bulkPublishMinorDocumentVersions field.
|
||||
func (r *mutationResolver) BulkPublishMinorDocumentVersions(ctx context.Context, input types.BulkPublishDocumentVersionsInput) (*types.BulkPublishDocumentVersionsPayload, error) {
|
||||
if len(input.DocumentIds) == 0 {
|
||||
return &types.BulkPublishDocumentVersionsPayload{
|
||||
DocumentVersions: []*types.DocumentVersion{},
|
||||
Documents: []*types.Document{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
for _, documentID := range input.DocumentIds {
|
||||
if err := r.authorize(ctx, documentID, probo.ActionDocumentVersionPublish); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
prb := r.ProboService(ctx, input.DocumentIds[0].TenantID())
|
||||
|
||||
versions, documents, err := prb.Documents.BulkPublishMinorVersions(ctx, probo.BulkPublishVersionsRequest{
|
||||
DocumentIDs: input.DocumentIds,
|
||||
Changelog: input.Changelog,
|
||||
})
|
||||
if err != nil {
|
||||
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
|
||||
return nil, gqlutils.Conflict(ctx, errArchived)
|
||||
}
|
||||
|
||||
if errNotDraft, ok := errors.AsType[*probo.ErrDocumentVersionNotDraft](err); ok {
|
||||
return nil, gqlutils.Invalid(ctx, errNotDraft)
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot bulk publish minor document versions", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
|
||||
@@ -2196,24 +2196,6 @@ func (r *Resolver) UpdateDocumentVersionTool(ctx context.Context, req *mcp.CallT
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) PublishDocumentVersionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.PublishDocumentVersionInput) (*mcp.CallToolResult, types.PublishDocumentVersionOutput, error) {
|
||||
r.MustAuthorize(ctx, input.DocumentID, probo.ActionDocumentVersionPublish)
|
||||
|
||||
svc := r.ProboService(ctx, input.DocumentID)
|
||||
|
||||
user := authn.IdentityFromContext(ctx)
|
||||
|
||||
document, documentVersion, err := svc.Documents.PublishVersion(ctx, input.DocumentID, user.ID, input.Changelog)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot publish document version: %w", err))
|
||||
}
|
||||
|
||||
return nil, types.PublishDocumentVersionOutput{
|
||||
Document: types.NewDocument(document),
|
||||
DocumentVersion: types.NewDocumentVersion(documentVersion),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) ListDocumentVersionSignaturesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListDocumentVersionSignaturesInput) (*mcp.CallToolResult, types.ListDocumentVersionSignaturesOutput, error) {
|
||||
r.MustAuthorize(ctx, input.DocumentVersionID, probo.ActionDocumentVersionSignatureList)
|
||||
|
||||
@@ -3324,3 +3306,47 @@ func (r *Resolver) RequestDocumentVersionApprovalTool(ctx context.Context, req *
|
||||
DocumentVersion: types.NewDocumentVersion(documentVersion),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) PublishMajorDocumentVersionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.PublishMajorDocumentVersionInput) (*mcp.CallToolResult, types.PublishMajorDocumentVersionOutput, error) {
|
||||
r.MustAuthorize(ctx, input.DocumentID, probo.ActionDocumentVersionPublish)
|
||||
|
||||
svc := r.ProboService(ctx, input.DocumentID)
|
||||
user := authn.IdentityFromContext(ctx)
|
||||
|
||||
document, documentVersion, err := svc.Documents.PublishMajorVersion(
|
||||
ctx,
|
||||
input.DocumentID,
|
||||
user.ID,
|
||||
input.Changelog,
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot publish major document version: %w", err))
|
||||
}
|
||||
|
||||
return nil, types.PublishMajorDocumentVersionOutput{
|
||||
Document: types.NewDocument(document),
|
||||
DocumentVersion: types.NewDocumentVersion(documentVersion),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) PublishMinorDocumentVersionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.PublishMinorDocumentVersionInput) (*mcp.CallToolResult, types.PublishMinorDocumentVersionOutput, error) {
|
||||
r.MustAuthorize(ctx, input.DocumentID, probo.ActionDocumentVersionPublish)
|
||||
|
||||
svc := r.ProboService(ctx, input.DocumentID)
|
||||
user := authn.IdentityFromContext(ctx)
|
||||
|
||||
document, documentVersion, err := svc.Documents.PublishMinorVersion(
|
||||
ctx,
|
||||
input.DocumentID,
|
||||
user.ID,
|
||||
input.Changelog,
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot publish minor document version: %w", err))
|
||||
}
|
||||
|
||||
return nil, types.PublishMinorDocumentVersionOutput{
|
||||
Document: types.NewDocument(document),
|
||||
DocumentVersion: types.NewDocumentVersion(documentVersion),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -5207,11 +5207,16 @@ components:
|
||||
classification:
|
||||
$ref: "#/components/schemas/DocumentClassification"
|
||||
description: Document classification
|
||||
current_published_version:
|
||||
current_published_major:
|
||||
type:
|
||||
- integer
|
||||
- "null"
|
||||
description: Current published version number
|
||||
description: Current published major version number
|
||||
current_published_minor:
|
||||
type:
|
||||
- integer
|
||||
- "null"
|
||||
description: Current published minor version number
|
||||
trust_center_visibility:
|
||||
$ref: "#/components/schemas/TrustCenterVisibility"
|
||||
description: Trust center visibility
|
||||
@@ -5240,7 +5245,8 @@ components:
|
||||
- organization_id
|
||||
- document_id
|
||||
- title
|
||||
- version_number
|
||||
- major
|
||||
- minor
|
||||
- classification
|
||||
- content
|
||||
- changelog
|
||||
@@ -5260,9 +5266,12 @@ components:
|
||||
title:
|
||||
type: string
|
||||
description: Document version title
|
||||
version_number:
|
||||
major:
|
||||
type: integer
|
||||
description: Version number
|
||||
description: Major version number
|
||||
minor:
|
||||
type: integer
|
||||
description: Minor version number
|
||||
classification:
|
||||
$ref: "#/components/schemas/DocumentClassification"
|
||||
description: Document classification
|
||||
@@ -5606,7 +5615,19 @@ components:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Deleted document version ID
|
||||
|
||||
PublishDocumentVersionInput:
|
||||
PublishMajorDocumentVersionInput:
|
||||
type: object
|
||||
required:
|
||||
- document_id
|
||||
properties:
|
||||
document_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Document ID
|
||||
changelog:
|
||||
type: string
|
||||
description: Changelog for this version
|
||||
|
||||
PublishMinorDocumentVersionInput:
|
||||
type: object
|
||||
required:
|
||||
- document_id
|
||||
@@ -7471,12 +7492,20 @@ tools:
|
||||
$ref: "#/components/schemas/DeleteDraftDocumentVersionInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/DeleteDraftDocumentVersionOutput"
|
||||
- name: publishDocumentVersion
|
||||
description: Publish a draft document version
|
||||
- name: publishMajorDocumentVersion
|
||||
description: Publish a draft document version as a new major version
|
||||
hints:
|
||||
readonly: false
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/PublishDocumentVersionInput"
|
||||
$ref: "#/components/schemas/PublishMajorDocumentVersionInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/PublishDocumentVersionOutput"
|
||||
- name: publishMinorDocumentVersion
|
||||
description: Publish a draft document version as a minor version
|
||||
hints:
|
||||
readonly: false
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/PublishMinorDocumentVersionInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/PublishDocumentVersionOutput"
|
||||
- name: requestDocumentVersionApproval
|
||||
|
||||
@@ -21,17 +21,18 @@ import (
|
||||
|
||||
func NewDocument(d *coredata.Document) *Document {
|
||||
return &Document{
|
||||
ID: d.ID,
|
||||
OrganizationID: d.OrganizationID,
|
||||
Title: d.Title,
|
||||
DocumentType: d.DocumentType,
|
||||
Classification: d.Classification,
|
||||
CurrentPublishedVersion: d.CurrentPublishedVersion,
|
||||
TrustCenterVisibility: d.TrustCenterVisibility,
|
||||
Status: d.Status,
|
||||
ArchivedAt: d.ArchivedAt,
|
||||
CreatedAt: d.CreatedAt,
|
||||
UpdatedAt: d.UpdatedAt,
|
||||
ID: d.ID,
|
||||
OrganizationID: d.OrganizationID,
|
||||
Title: d.Title,
|
||||
DocumentType: d.DocumentType,
|
||||
Classification: d.Classification,
|
||||
CurrentPublishedMajor: d.CurrentPublishedMajor,
|
||||
CurrentPublishedMinor: d.CurrentPublishedMinor,
|
||||
TrustCenterVisibility: d.TrustCenterVisibility,
|
||||
Status: d.Status,
|
||||
ArchivedAt: d.ArchivedAt,
|
||||
CreatedAt: d.CreatedAt,
|
||||
UpdatedAt: d.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +85,8 @@ func NewDocumentVersion(dv *coredata.DocumentVersion) *DocumentVersion {
|
||||
OrganizationID: dv.OrganizationID,
|
||||
DocumentID: dv.DocumentID,
|
||||
Title: dv.Title,
|
||||
VersionNumber: dv.VersionNumber,
|
||||
Major: dv.Major,
|
||||
Minor: dv.Minor,
|
||||
Classification: dv.Classification,
|
||||
Content: dv.Content,
|
||||
Changelog: dv.Changelog,
|
||||
|
||||
Reference in New Issue
Block a user