From 68bc8cf59a33bea14ecda9ca00f476b5ad206edc Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Fri, 27 Mar 2026 19:57:58 +0100 Subject: [PATCH] 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 --- .../CompliancePageDocumentList.tsx | 4 +- .../_components/DocumentActionsDropdown.tsx | 7 +- .../_components/DocumentLayoutDrawer.tsx | 7 +- .../_components/DocumentListItem.tsx | 7 +- .../DocumentVersionsDropdownItem.tsx | 7 +- .../documents/_components/PublishDialog.tsx | 156 ++++++----- .../_components/PublishDocumentsDialog.tsx | 121 ++++++--- .../_components/UpdateVersionDialog.tsx | 3 +- .../documents/approve/DocumentApprovePage.tsx | 18 +- .../employee/_components/VersionRow.tsx | 7 +- e2e/console/document_version_test.go | 25 +- pkg/coredata/document.go | 130 +++++---- pkg/coredata/document_filter.go | 4 +- pkg/coredata/document_version.go | 46 ++-- .../document_version_approval_quorum.go | 55 ++-- pkg/coredata/document_version_signature.go | 89 ++++-- pkg/coredata/migrations/20260326T120000Z.sql | 11 + pkg/docgen/generator.go | 3 +- pkg/docgen/generator_test.go | 6 +- pkg/docgen/template.html | 2 +- pkg/probo/document_approval_service.go | 29 +- pkg/probo/document_service.go | 254 +++++++++++++----- pkg/server/api/console/v1/schema.graphql | 28 +- pkg/server/api/console/v1/types/document.go | 17 +- .../api/console/v1/types/document_version.go | 3 +- .../api/console/v1/types/employee_document.go | 3 +- pkg/server/api/console/v1/v1_resolver.go | 114 +++++++- pkg/server/api/mcp/v1/schema.resolvers.go | 62 +++-- pkg/server/api/mcp/v1/specification.yaml | 47 +++- pkg/server/api/mcp/v1/types/document.go | 26 +- pkg/trust/document_service.go | 3 +- 31 files changed, 877 insertions(+), 417 deletions(-) create mode 100644 pkg/coredata/migrations/20260326T120000Z.sql diff --git a/apps/console/src/pages/organizations/compliance-page/documents/_components/CompliancePageDocumentList.tsx b/apps/console/src/pages/organizations/compliance-page/documents/_components/CompliancePageDocumentList.tsx index 25c98d56a..21f50ab20 100644 --- a/apps/console/src/pages/organizations/compliance-page/documents/_components/CompliancePageDocumentList.tsx +++ b/apps/console/src/pages/organizations/compliance-page/documents/_components/CompliancePageDocumentList.tsx @@ -16,7 +16,7 @@ const fragment = graphql` edges { node { id - currentPublishedVersion + currentPublishedMajor ...CompliancePageDocumentListItem_documentFragment } } @@ -30,7 +30,7 @@ export function CompliancePageDocumentList(props: { fragmentRef: CompliancePageD const { __ } = useTranslate(); const { compliancePage, documents } = useFragment(fragment, fragmentRef); - const publishedDocuments = documents.edges.filter(({ node }) => node.currentPublishedVersion != null); + const publishedDocuments = documents.edges.filter(({ node }) => node.currentPublishedMajor != null); return (
diff --git a/apps/console/src/pages/organizations/documents/_components/DocumentActionsDropdown.tsx b/apps/console/src/pages/organizations/documents/_components/DocumentActionsDropdown.tsx index edcbb23db..62e1a8627 100644 --- a/apps/console/src/pages/organizations/documents/_components/DocumentActionsDropdown.tsx +++ b/apps/console/src/pages/organizations/documents/_components/DocumentActionsDropdown.tsx @@ -75,7 +75,8 @@ const unarchiveDocumentMutation = graphql` const versionFragment = graphql` fragment DocumentActionsDropdown_versionFragment on DocumentVersion { id - version + major + minor status canDeleteDraft: permission(action: "core:document-version:delete-draft") } @@ -229,7 +230,7 @@ export function DocumentActionsDropdownn(props: { __( "This will permanently delete the draft version %s of \"%s\". This action cannot be undone.", ), - version.version, + `${version.major}.${version.minor}`, document.title, ), }, @@ -259,7 +260,7 @@ export function DocumentActionsDropdownn(props: { if (data.exportDocumentVersionPDF) { const link = window.document.createElement("a"); link.href = data.exportDocumentVersionPDF.data; - link.download = `${document.title}-v${version.version}.pdf`; + link.download = `${document.title}-v${version.major}.${version.minor}.pdf`; window.document.body.appendChild(link); link.click(); window.document.body.removeChild(link); diff --git a/apps/console/src/pages/organizations/documents/_components/DocumentLayoutDrawer.tsx b/apps/console/src/pages/organizations/documents/_components/DocumentLayoutDrawer.tsx index a43efb02f..d7eb2462f 100644 --- a/apps/console/src/pages/organizations/documents/_components/DocumentLayoutDrawer.tsx +++ b/apps/console/src/pages/organizations/documents/_components/DocumentLayoutDrawer.tsx @@ -29,7 +29,8 @@ const versionFragment = graphql` fragment DocumentLayoutDrawer_versionFragment on DocumentVersion { id classification - version + major + minor status updatedAt publishedAt @@ -202,7 +203,9 @@ export function DocumentLayoutDrawer(props: {
- {version.version} + {version.major} + . + {version.minor}
diff --git a/apps/console/src/pages/organizations/documents/_components/DocumentListItem.tsx b/apps/console/src/pages/organizations/documents/_components/DocumentListItem.tsx index e22c72549..8e0b5b5d7 100644 --- a/apps/console/src/pages/organizations/documents/_components/DocumentListItem.tsx +++ b/apps/console/src/pages/organizations/documents/_components/DocumentListItem.tsx @@ -22,7 +22,8 @@ const fragment = graphql` node { id status - version + major + minor approvalQuorums(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) { edges { node { @@ -148,7 +149,9 @@ export function DocumentListItem(props: { v - {lastVersion.version} + {lastVersion.major} + . + {lastVersion.minor} {getDocumentTypeLabel(__, document.documentType)} diff --git a/apps/console/src/pages/organizations/documents/_components/DocumentVersionsDropdownItem.tsx b/apps/console/src/pages/organizations/documents/_components/DocumentVersionsDropdownItem.tsx index 9fea76124..e03a6db27 100644 --- a/apps/console/src/pages/organizations/documents/_components/DocumentVersionsDropdownItem.tsx +++ b/apps/console/src/pages/organizations/documents/_components/DocumentVersionsDropdownItem.tsx @@ -13,7 +13,8 @@ import { useOrganizationId } from "#/hooks/useOrganizationId"; const fragment = graphql` fragment DocumentVersionsDropdownItemFragment on DocumentVersion { id - version + major + minor status publishedAt updatedAt @@ -51,7 +52,9 @@ export function DocumentVersionsDropdownItem(props: { )} >
- {version.version} + {version.major} + . + {version.minor}
diff --git a/apps/console/src/pages/organizations/documents/_components/PublishDialog.tsx b/apps/console/src/pages/organizations/documents/_components/PublishDialog.tsx index 711693795..f0bcf7537 100644 --- a/apps/console/src/pages/organizations/documents/_components/PublishDialog.tsx +++ b/apps/console/src/pages/organizations/documents/_components/PublishDialog.tsx @@ -18,7 +18,8 @@ import { graphql } from "relay-runtime"; import { z } from "zod"; import type { PublishDialog_documentFragment$key } from "#/__generated__/core/PublishDialog_documentFragment.graphql"; -import type { PublishDialog_publishMutation } from "#/__generated__/core/PublishDialog_publishMutation.graphql"; +import type { PublishDialog_publishMajorMutation } from "#/__generated__/core/PublishDialog_publishMajorMutation.graphql"; +import type { PublishDialog_publishMinorMutation } from "#/__generated__/core/PublishDialog_publishMinorMutation.graphql"; import type { PublishDialog_requestApprovalMutation } from "#/__generated__/core/PublishDialog_requestApprovalMutation.graphql"; import { PeopleMultiSelectField } from "#/components/form/PeopleMultiSelectField"; import { useFormWithSchema } from "#/hooks/useFormWithSchema"; @@ -62,9 +63,24 @@ const documentFragment = graphql` } `; -const publishMutation = graphql` - mutation PublishDialog_publishMutation($input: PublishDocumentVersionInput!) { - publishDocumentVersion(input: $input) { +const publishMajorMutation = graphql` + mutation PublishDialog_publishMajorMutation($input: PublishMajorDocumentVersionInput!) { + publishMajorDocumentVersion(input: $input) { + document { + id + status + } + documentVersion { + id + status + } + } + } +`; + +const publishMinorMutation = graphql` + mutation PublishDialog_publishMinorMutation($input: PublishMinorDocumentVersionInput!) { + publishMinorDocumentVersion(input: $input) { document { id status @@ -160,36 +176,52 @@ export function PublishDialog({ }, })); - const [publishVersion, isPublishing] = useMutation(publishMutation); - const [requestApproval, isRequesting] = useMutation(requestApprovalMutation); + const [publishMajor, isPublishingMajor] + = useMutation(publishMajorMutation); + const [publishMinor, isPublishingMinor] + = useMutation(publishMinorMutation); + const [requestApproval, isRequesting] + = useMutation(requestApprovalMutation); - const isBusy = isPublishing || isRequesting; + const isBusy = isPublishingMajor || isPublishingMinor || isRequesting; const approverIds = watch("approverIds"); - const actionRef = useRef<"publish" | "request-approval">("publish"); + const actionRef = useRef<"publish" | "publish-minor" | "request-approval">("publish"); - const handlePublish = (data: z.infer) => { - publishVersion({ + const onPublishCompleted = (_: unknown, errors: ReadonlyArray<{ message: string }> | null) => { + if (errors?.length) { + toast({ + title: __("Error"), + description: formatError(__("Failed to publish document"), [...errors]), + variant: "error", + }); + } else { + toast({ + title: __("Success"), + description: __("Document published successfully."), + variant: "success", + }); + dialogRef.current?.close(); + onSuccess(); + } + }; + + const onPublishError = (error: Error) => { + toast({ title: __("Error"), description: error.message, variant: "error" }); + }; + + const handlePublishMajor = (data: z.infer) => { + publishMajor({ variables: { input: { documentId, changelog: data.changelog } }, - onCompleted(_, errors) { - if (errors?.length) { - toast({ - title: __("Error"), - description: formatError(__("Failed to publish document"), errors), - variant: "error", - }); - } else { - toast({ - title: __("Success"), - description: __("Document published successfully."), - variant: "success", - }); - dialogRef.current?.close(); - onSuccess(); - } - }, - onError(error) { - toast({ title: __("Error"), description: error.message, variant: "error" }); - }, + onCompleted: onPublishCompleted, + onError: onPublishError, + }); + }; + + const handlePublishMinor = (data: z.infer) => { + publishMinor({ + variables: { input: { documentId, changelog: data.changelog } }, + onCompleted: onPublishCompleted, + onError: onPublishError, }); }; @@ -231,7 +263,9 @@ export function PublishDialog({
void handleSubmit((data) => { if (actionRef.current === "publish") { - handlePublish(data); + handlePublishMajor(data); + } else if (actionRef.current === "publish-minor") { + handlePublishMinor(data); } else { onRequestApproval(data); } @@ -284,38 +318,34 @@ export function PublishDialog({
- {hasPendingApproval - ? ( - - ) - : ( - <> - - - - )} + + + {!hasPendingApproval && ( + + )} diff --git a/apps/console/src/pages/organizations/documents/_components/PublishDocumentsDialog.tsx b/apps/console/src/pages/organizations/documents/_components/PublishDocumentsDialog.tsx index 1e722ca27..15589b4b0 100644 --- a/apps/console/src/pages/organizations/documents/_components/PublishDocumentsDialog.tsx +++ b/apps/console/src/pages/organizations/documents/_components/PublishDocumentsDialog.tsx @@ -5,17 +5,19 @@ import { Dialog, DialogContent, DialogFooter, + IconUpload, IconWarning, Textarea, useDialogRef, useToast, } from "@probo/ui"; -import { type ReactNode } from "react"; +import { type ReactNode, useRef } from "react"; import { useMutation } from "react-relay"; import { graphql } from "relay-runtime"; import { z } from "zod"; -import type { PublishDocumentsDialogMutation } from "#/__generated__/core/PublishDocumentsDialogMutation.graphql"; +import type { PublishDocumentsDialog_majorMutation } from "#/__generated__/core/PublishDocumentsDialog_majorMutation.graphql"; +import type { PublishDocumentsDialog_minorMutation } from "#/__generated__/core/PublishDocumentsDialog_minorMutation.graphql"; import { useFormWithSchema } from "#/hooks/useFormWithSchema"; type Props = { @@ -24,11 +26,27 @@ type Props = { onSave: () => void; }; -const documentsPublishMutation = graphql` - mutation PublishDocumentsDialogMutation( +const publishMajorMutation = graphql` + mutation PublishDocumentsDialog_majorMutation( $input: BulkPublishDocumentVersionsInput! ) { - bulkPublishDocumentVersions(input: $input) { + bulkPublishMajorDocumentVersions(input: $input) { + documentVersions { + id + } + documents { + id + ...DocumentListItemFragment + } + } + } +`; + +const publishMinorMutation = graphql` + mutation PublishDocumentsDialog_minorMutation( + $input: BulkPublishDocumentVersionsInput! + ) { + bulkPublishMinorDocumentVersions(input: $input) { documentVersions { id } @@ -48,12 +66,18 @@ export function PublishDocumentsDialog({ const { __ } = useTranslate(); const { toast } = useToast(); const dialogRef = useDialogRef(); + const actionRef = useRef<"major" | "minor">("major"); const schema = z.object({ changelog: z.string().min(1, __("Changelog is required")), }); - const [publishMutation, isPublishing] = useMutation(documentsPublishMutation); + const [publishMajor, isPublishingMajor] + = useMutation(publishMajorMutation); + const [publishMinor, isPublishingMinor] + = useMutation(publishMinorMutation); + + const isBusy = isPublishingMajor || isPublishingMinor; const { handleSubmit, @@ -65,41 +89,47 @@ export function PublishDocumentsDialog({ }, }); - const onSubmit = (data: z.infer) => { - publishMutation({ - variables: { - input: { - documentIds, - changelog: data.changelog, - }, - }, - onCompleted(_, errors) { - if (errors?.length) { - toast({ - title: __("Error"), - description: formatError(__("Failed to publish documents"), errors), - variant: "error", - }); - } else { - toast({ - title: __("Success"), - description: sprintf(__("%s documents published"), documentIds.length), - variant: "success", - }); - dialogRef.current?.close(); - onSave(); - } - }, - onError(error) { - toast({ - title: __("Error"), - description: error.message, - variant: "error", - }); - }, + const onCompleted = (_: unknown, errors: ReadonlyArray<{ message: string }> | null) => { + if (errors?.length) { + toast({ + title: __("Error"), + description: formatError(__("Failed to publish documents"), [...errors]), + variant: "error", + }); + } else { + toast({ + title: __("Success"), + description: sprintf(__("%s documents published"), documentIds.length), + variant: "success", + }); + dialogRef.current?.close(); + onSave(); + } + }; + + const onError = (error: Error) => { + toast({ + title: __("Error"), + description: error.message, + variant: "error", }); }; + const onSubmit = (data: z.infer) => { + const variables = { + input: { + documentIds, + changelog: data.changelog, + }, + }; + + if (actionRef.current === "minor") { + publishMinor({ variables, onCompleted, onError }); + } else { + publishMajor({ variables, onCompleted, onError }); + } + }; + return ( - + diff --git a/apps/console/src/pages/organizations/documents/_components/UpdateVersionDialog.tsx b/apps/console/src/pages/organizations/documents/_components/UpdateVersionDialog.tsx index 6dcf7a335..7db71d0d8 100644 --- a/apps/console/src/pages/organizations/documents/_components/UpdateVersionDialog.tsx +++ b/apps/console/src/pages/organizations/documents/_components/UpdateVersionDialog.tsx @@ -51,7 +51,8 @@ const createDraftDocument = graphql` content status publishedAt - version + major + minor updatedAt signatures(first: 100) { edges { diff --git a/apps/console/src/pages/organizations/documents/approve/DocumentApprovePage.tsx b/apps/console/src/pages/organizations/documents/approve/DocumentApprovePage.tsx index 32f02258c..669c6d686 100644 --- a/apps/console/src/pages/organizations/documents/approve/DocumentApprovePage.tsx +++ b/apps/console/src/pages/organizations/documents/approve/DocumentApprovePage.tsx @@ -70,9 +70,11 @@ const documentFragment = graphql` const versionRowFragment = graphql` fragment DocumentApprovePageVersionRowFragment on EmployeeDocumentVersion { id - version + major + minor publishedAt approvalDecision { + id state } } @@ -96,10 +98,7 @@ const approveDocumentVersionMutation = graphql` ) { approveDocumentVersion(input: $input) { approvalDecision { - id - state - decidedAt - comment + ...DocumentApprovePageDecisionFragment } } } @@ -111,10 +110,7 @@ const rejectDocumentVersionMutation = graphql` ) { rejectDocumentVersion(input: $input) { approvalDecision { - id - state - decidedAt - comment + ...DocumentApprovePageDecisionFragment } } } @@ -192,14 +188,14 @@ function VersionRow({ )} > {versionData.publishedAt - ? `v${versionData.version} - ${(() => { + ? `v${versionData.major}.${versionData.minor} - ${(() => { const date = new Date(versionData.publishedAt); const day = String(date.getDate()).padStart(2, "0"); const month = String(date.getMonth() + 1).padStart(2, "0"); const year = date.getFullYear(); return `${day}/${month}/${year}`; })()}` - : `v${versionData.version}`} + : `v${versionData.major}.${versionData.minor}`}

diff --git a/apps/console/src/pages/organizations/employee/_components/VersionRow.tsx b/apps/console/src/pages/organizations/employee/_components/VersionRow.tsx index 3d9b01845..98408b8ae 100644 --- a/apps/console/src/pages/organizations/employee/_components/VersionRow.tsx +++ b/apps/console/src/pages/organizations/employee/_components/VersionRow.tsx @@ -9,7 +9,8 @@ const fragment = graphql` fragment VersionRowFragment on EmployeeDocumentVersion { # eslint-disable-next-line relay/unused-fields id - version + major + minor signed publishedAt } @@ -55,14 +56,14 @@ export function VersionRow({ )} > {versionData.publishedAt - ? `v${versionData.version} - ${(() => { + ? `v${versionData.major}.${versionData.minor} - ${(() => { const date = new Date(versionData.publishedAt); const day = String(date.getDate()).padStart(2, "0"); const month = String(date.getMonth() + 1).padStart(2, "0"); const year = date.getFullYear(); return `${day}/${month}/${year}`; })()}` - : `v${versionData.version}`} + : `v${versionData.major}.${versionData.minor}`}

diff --git a/e2e/console/document_version_test.go b/e2e/console/document_version_test.go index 5860481e8..9c8f07bc3 100644 --- a/e2e/console/document_version_test.go +++ b/e2e/console/document_version_test.go @@ -223,7 +223,8 @@ func TestDocumentVersion_PublishVersion(t *testing.T) { node { id status - version + major + minor } } } @@ -237,9 +238,10 @@ func TestDocumentVersion_PublishVersion(t *testing.T) { Versions struct { Edges []struct { Node struct { - ID string `json:"id"` - Status string `json:"status"` - Version int `json:"version"` + ID string `json:"id"` + Status string `json:"status"` + Major int `json:"major"` + Minor int `json:"minor"` } `json:"node"` } `json:"edges"` } `json:"versions"` @@ -251,7 +253,8 @@ func TestDocumentVersion_PublishVersion(t *testing.T) { require.NotEmpty(t, result.Node.Versions.Edges) assert.Equal(t, "PUBLISHED", result.Node.Versions.Edges[0].Node.Status) - assert.Equal(t, 1, result.Node.Versions.Edges[0].Node.Version) + assert.Equal(t, 1, result.Node.Versions.Edges[0].Node.Major) + assert.Equal(t, 0, result.Node.Versions.Edges[0].Node.Minor) } func TestDocumentVersion_CreateDraft(t *testing.T) { @@ -435,8 +438,8 @@ func TestDocumentVersion_BulkPublish(t *testing.T) { approveTestDocument(t, owner, docID2) query := ` - mutation BulkPublishDocumentVersions($input: BulkPublishDocumentVersionsInput!) { - bulkPublishDocumentVersions(input: $input) { + mutation BulkPublishMajorDocumentVersions($input: BulkPublishDocumentVersionsInput!) { + bulkPublishMajorDocumentVersions(input: $input) { documentVersions { id status @@ -446,12 +449,12 @@ func TestDocumentVersion_BulkPublish(t *testing.T) { ` var result struct { - BulkPublishDocumentVersions struct { + BulkPublishMajorDocumentVersions struct { DocumentVersions []struct { ID string `json:"id"` Status string `json:"status"` } `json:"documentVersions"` - } `json:"bulkPublishDocumentVersions"` + } `json:"bulkPublishMajorDocumentVersions"` } err := owner.Execute(query, map[string]any{ @@ -462,8 +465,8 @@ func TestDocumentVersion_BulkPublish(t *testing.T) { }, &result) require.NoError(t, err) - assert.Equal(t, 2, len(result.BulkPublishDocumentVersions.DocumentVersions)) - for _, dv := range result.BulkPublishDocumentVersions.DocumentVersions { + assert.Equal(t, 2, len(result.BulkPublishMajorDocumentVersions.DocumentVersions)) + for _, dv := range result.BulkPublishMajorDocumentVersions.DocumentVersions { assert.Equal(t, "PUBLISHED", dv.Status) } } diff --git a/pkg/coredata/document.go b/pkg/coredata/document.go index 17a978937..e7d2509d0 100644 --- a/pkg/coredata/document.go +++ b/pkg/coredata/document.go @@ -30,17 +30,18 @@ import ( type ( Document struct { - ID gid.GID `db:"id"` - OrganizationID gid.GID `db:"organization_id"` - Title string `db:"title"` - DocumentType DocumentType `db:"document_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"` + ID gid.GID `db:"id"` + OrganizationID gid.GID `db:"organization_id"` + Title string `db:"title"` + DocumentType DocumentType `db:"document_type"` + Classification DocumentClassification `db:"classification"` + CurrentPublishedMajor *int `db:"current_published_major"` + CurrentPublishedMinor *int `db:"current_published_minor"` + 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"` } Documents []*Document @@ -128,7 +129,8 @@ SELECT title, document_type, classification, - current_published_version, + current_published_major, + current_published_minor, trust_center_visibility, status, archived_at, @@ -181,7 +183,8 @@ SELECT title, document_type, classification, - current_published_version, + current_published_major, + current_published_minor, trust_center_visibility, status, archived_at, @@ -235,7 +238,8 @@ SELECT title, document_type, classification, - current_published_version, + current_published_major, + current_published_minor, trust_center_visibility, status, archived_at, @@ -318,7 +322,8 @@ SELECT title, document_type, classification, - current_published_version, + current_published_major, + current_published_minor, trust_center_visibility, status, archived_at, @@ -370,7 +375,8 @@ SELECT title, document_type, classification, - current_published_version, + current_published_major, + current_published_minor, trust_center_visibility, status, archived_at, @@ -424,7 +430,8 @@ WITH published_documents AS ( documents d LEFT JOIN document_versions dv ON dv.document_id = d.id - AND dv.version_number = d.current_published_version + AND dv.major = d.current_published_major + AND dv.minor = d.current_published_minor WHERE d.deleted_at IS NULL AND d.organization_id = @organization_id @@ -435,7 +442,8 @@ SELECT COALESCE(published_title, title) AS title, document_type, classification, - current_published_version, + current_published_major, + current_published_minor, trust_center_visibility, status, archived_at, @@ -484,7 +492,8 @@ INSERT INTO title, document_type, classification, - current_published_version, + current_published_major, + current_published_minor, trust_center_visibility, status, archived_at, @@ -498,7 +507,8 @@ VALUES ( @title, @document_type, @classification, - @current_published_version, + @current_published_major, + @current_published_minor, @trust_center_visibility, @status, @archived_at, @@ -508,18 +518,19 @@ VALUES ( ` args := pgx.StrictNamedArgs{ - "tenant_id": scope.GetTenantID(), - "document_id": p.ID, - "organization_id": p.OrganizationID, - "title": p.Title, - "document_type": p.DocumentType, - "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, + "tenant_id": scope.GetTenantID(), + "document_id": p.ID, + "organization_id": p.OrganizationID, + "title": p.Title, + "document_type": p.DocumentType, + "classification": p.Classification, + "current_published_major": p.CurrentPublishedMajor, + "current_published_minor": p.CurrentPublishedMinor, + "trust_center_visibility": p.TrustCenterVisibility, + "status": p.Status, + "archived_at": p.ArchivedAt, + "created_at": p.CreatedAt, + "updated_at": p.UpdatedAt, } _, err := conn.Exec(ctx, q, args) return err @@ -572,7 +583,8 @@ UPDATE documents SET title = @title, - current_published_version = @current_published_version, + current_published_major = @current_published_major, + current_published_minor = @current_published_minor, document_type = @document_type, classification = @classification, trust_center_visibility = @trust_center_visibility, @@ -587,15 +599,16 @@ WHERE q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{ - "document_id": p.ID, - "updated_at": time.Now(), - "title": p.Title, - "current_published_version": p.CurrentPublishedVersion, - "document_type": p.DocumentType, - "classification": p.Classification, - "trust_center_visibility": p.TrustCenterVisibility, - "status": p.Status, - "archived_at": p.ArchivedAt, + "document_id": p.ID, + "updated_at": time.Now(), + "title": p.Title, + "current_published_major": p.CurrentPublishedMajor, + "current_published_minor": p.CurrentPublishedMinor, + "document_type": p.DocumentType, + "classification": p.Classification, + "trust_center_visibility": p.TrustCenterVisibility, + "status": p.Status, + "archived_at": p.ArchivedAt, } maps.Copy(args, scope.SQLArguments()) @@ -666,7 +679,8 @@ SELECT scoped_documents.title, scoped_documents.document_type, scoped_documents.classification, - scoped_documents.current_published_version, + scoped_documents.current_published_major, + scoped_documents.current_published_minor, scoped_documents.trust_center_visibility, scoped_documents.status, scoped_documents.archived_at, @@ -757,7 +771,8 @@ SELECT scoped_documents.title, scoped_documents.document_type, scoped_documents.classification, - scoped_documents.current_published_version, + scoped_documents.current_published_major, + scoped_documents.current_published_minor, scoped_documents.trust_center_visibility, scoped_documents.status, scoped_documents.archived_at, @@ -874,28 +889,29 @@ func (p *Document) IsLastSignableVersionSignedByUserEmail( userEmail mail.Addr, ) (bool, error) { q := ` -WITH last_signable_version AS ( +WITH max_signable_major AS ( + SELECT MAX(dv.major) AS major + FROM document_versions dv + INNER JOIN document_version_signatures dvs ON dvs.document_version_id = dv.id + INNER JOIN iam_membership_profiles p ON dvs.signed_by_profile_id = p.id + INNER JOIN identities i ON p.identity_id = i.id + WHERE dv.document_id = @document_id + AND i.email_address = @user_email::CITEXT +), +last_signable_version AS ( SELECT d.id AS document_id, d.tenant_id, - dv.version_number, + dv.major, dvs.state FROM documents d INNER JOIN document_versions dv ON dv.document_id = d.id + INNER JOIN max_signable_major msm ON dv.major = msm.major INNER JOIN document_version_signatures dvs ON dvs.document_version_id = dv.id INNER JOIN iam_membership_profiles p ON dvs.signed_by_profile_id = p.id INNER JOIN identities i ON p.identity_id = i.id WHERE d.id = @document_id AND i.email_address = @user_email::CITEXT - AND dv.version_number = ( - SELECT MAX(dv2.version_number) - FROM document_versions dv2 - INNER JOIN document_version_signatures dvs2 ON dvs2.document_version_id = dv2.id - INNER JOIN iam_membership_profiles p2 ON dvs2.signed_by_profile_id = p2.id - INNER JOIN identities i2 ON p2.identity_id = i2.id - WHERE dv2.document_id = d.id - AND i2.email_address = @user_email::CITEXT - ) ) SELECT EXISTS ( SELECT 1 @@ -938,7 +954,7 @@ WITH viewer_decision AS ( SELECT dvad.tenant_id, dvad.state, - dv.version_number, + dv.major, dvaq.created_at AS quorum_created_at FROM documents d INNER JOIN document_versions dv ON dv.document_id = d.id @@ -951,7 +967,7 @@ WITH viewer_decision AS ( SELECT state FROM viewer_decision WHERE %s -ORDER BY version_number DESC, quorum_created_at DESC +ORDER BY major DESC, quorum_created_at DESC LIMIT 1 ` diff --git a/pkg/coredata/document_filter.go b/pkg/coredata/document_filter.go index 9f29ea2de..0a6781769 100644 --- a/pkg/coredata/document_filter.go +++ b/pkg/coredata/document_filter.go @@ -131,8 +131,8 @@ func (f *DocumentFilter) SQLFragment() string { AND CASE WHEN @published::boolean IS NULL THEN TRUE - WHEN @published::boolean IS TRUE THEN current_published_version IS NOT NULL - WHEN @published::boolean IS FALSE THEN current_published_version IS NULL + WHEN @published::boolean IS TRUE THEN current_published_major IS NOT NULL + WHEN @published::boolean IS FALSE THEN current_published_major IS NULL END AND CASE diff --git a/pkg/coredata/document_version.go b/pkg/coredata/document_version.go index 1bba3d44f..f3a573ae6 100644 --- a/pkg/coredata/document_version.go +++ b/pkg/coredata/document_version.go @@ -34,7 +34,8 @@ type ( OrganizationID gid.GID `db:"organization_id"` DocumentID gid.GID `db:"document_id"` Title string `db:"title"` - VersionNumber int `db:"version_number"` + Major int `db:"major"` + Minor int `db:"minor"` Classification DocumentClassification `db:"classification"` Content string `db:"content"` Changelog string `db:"changelog"` @@ -116,7 +117,8 @@ SELECT organization_id, document_id, title, - version_number, + major, + minor, classification, content, changelog, @@ -177,7 +179,8 @@ SELECT organization_id, document_id, title, - version_number, + major, + minor, classification, content, changelog, @@ -227,7 +230,8 @@ INSERT INTO document_versions ( organization_id, document_id, title, - version_number, + major, + minor, classification, content, changelog, @@ -241,7 +245,8 @@ VALUES ( @organization_id, @document_id, @title, - @version_number, + @major, + @minor, @classification, @content, @changelog, @@ -256,7 +261,8 @@ VALUES ( "organization_id": dv.OrganizationID, "document_id": dv.DocumentID, "title": dv.Title, - "version_number": dv.VersionNumber, + "major": dv.Major, + "minor": dv.Minor, "classification": dv.Classification, "content": dv.Content, "changelog": dv.Changelog, @@ -270,7 +276,7 @@ VALUES ( var pgErr *pgconn.PgError if errors.As(err, &pgErr) { if pgErr.Code == "23505" { - if pgErr.ConstraintName == "document_versions_document_id_version_number_key" || pgErr.ConstraintName == "document_one_draft_version_idx" { + if pgErr.ConstraintName == "document_versions_document_id_major_minor_key" || pgErr.ConstraintName == "document_one_draft_version_idx" { return ErrResourceAlreadyExists } } @@ -281,12 +287,13 @@ VALUES ( return nil } -func (dv *DocumentVersion) LoadByDocumentIDAndVersionNumber( +func (dv *DocumentVersion) LoadByDocumentIDAndVersion( ctx context.Context, conn pg.Conn, scope Scoper, documentID gid.GID, - versionNumber int, + major int, + minor int, ) error { q := ` SELECT @@ -294,7 +301,8 @@ SELECT organization_id, document_id, title, - version_number, + major, + minor, classification, content, changelog, @@ -307,15 +315,17 @@ FROM WHERE %s AND document_id = @document_id - AND version_number = @version_number + AND major = @major + AND minor = @minor LIMIT 1; ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{ - "document_id": documentID, - "version_number": versionNumber, + "document_id": documentID, + "major": major, + "minor": minor, } maps.Copy(args, scope.SQLArguments()) @@ -346,7 +356,8 @@ SELECT organization_id, document_id, title, - version_number, + major, + minor, classification, content, changelog, @@ -396,7 +407,8 @@ SELECT organization_id, document_id, title, - version_number, + major, + minor, classification, content, changelog, @@ -444,6 +456,8 @@ func (dv DocumentVersion) Update( q := ` UPDATE document_versions SET title = @title, + major = @major, + minor = @minor, changelog = @changelog, status = @status, content = @content, @@ -459,6 +473,8 @@ WHERE %s args := pgx.StrictNamedArgs{ "document_version_id": dv.ID, "title": dv.Title, + "major": dv.Major, + "minor": dv.Minor, "changelog": dv.Changelog, "status": dv.Status, "content": dv.Content, diff --git a/pkg/coredata/document_version_approval_quorum.go b/pkg/coredata/document_version_approval_quorum.go index 65e0d7f67..1bbe15b00 100644 --- a/pkg/coredata/document_version_approval_quorum.go +++ b/pkg/coredata/document_version_approval_quorum.go @@ -115,19 +115,26 @@ func (q *DocumentVersionApprovalQuorum) LoadLastByDocumentVersionID( documentVersionID gid.GID, ) error { query := ` +WITH source_version AS ( + SELECT document_id, major FROM document_versions WHERE id = @version_id +), +major_versions AS ( + SELECT dv.id FROM document_versions dv + INNER JOIN source_version sv ON dv.document_id = sv.document_id AND dv.major = sv.major +) SELECT - id, - organization_id, - version_id, - status, - created_at, - updated_at + document_version_approval_quorums.id, + document_version_approval_quorums.organization_id, + document_version_approval_quorums.version_id, + document_version_approval_quorums.status, + document_version_approval_quorums.created_at, + document_version_approval_quorums.updated_at FROM document_version_approval_quorums +INNER JOIN major_versions mv ON document_version_approval_quorums.version_id = mv.id WHERE %s - AND version_id = @version_id -ORDER BY created_at DESC +ORDER BY document_version_approval_quorums.created_at DESC LIMIT 1 ` @@ -162,18 +169,25 @@ func (q *DocumentVersionApprovalQuorums) LoadAllByDocumentVersionID( cursor *page.Cursor[DocumentVersionApprovalQuorumOrderField], ) error { query := ` +WITH source_version AS ( + SELECT document_id, major FROM document_versions WHERE id = @version_id +), +major_versions AS ( + SELECT dv.id FROM document_versions dv + INNER JOIN source_version sv ON dv.document_id = sv.document_id AND dv.major = sv.major +) SELECT - id, - organization_id, - version_id, - status, - created_at, - updated_at + document_version_approval_quorums.id, + document_version_approval_quorums.organization_id, + document_version_approval_quorums.version_id, + document_version_approval_quorums.status, + document_version_approval_quorums.created_at, + document_version_approval_quorums.updated_at FROM document_version_approval_quorums +INNER JOIN major_versions mv ON document_version_approval_quorums.version_id = mv.id WHERE %s - AND version_id = @version_id AND %s ` @@ -205,13 +219,20 @@ func (q *DocumentVersionApprovalQuorums) CountByDocumentVersionID( documentVersionID gid.GID, ) (int, error) { query := ` +WITH source_version AS ( + SELECT document_id, major FROM document_versions WHERE id = @version_id +), +major_versions AS ( + SELECT dv.id FROM document_versions dv + INNER JOIN source_version sv ON dv.document_id = sv.document_id AND dv.major = sv.major +) SELECT - COUNT(id) + COUNT(document_version_approval_quorums.id) FROM document_version_approval_quorums +INNER JOIN major_versions mv ON document_version_approval_quorums.version_id = mv.id WHERE %s - AND version_id = @version_id ` query = fmt.Sprintf(query, scope.SQLFragment()) diff --git a/pkg/coredata/document_version_signature.go b/pkg/coredata/document_version_signature.go index 00a48cdbf..3122d3155 100644 --- a/pkg/coredata/document_version_signature.go +++ b/pkg/coredata/document_version_signature.go @@ -236,21 +236,28 @@ func (pvss *DocumentVersionSignatures) LoadByDocumentVersionID( filter *DocumentVersionSignatureFilter, ) error { q := ` +WITH source_version AS ( + SELECT document_id, major FROM document_versions WHERE id = @document_version_id +), +major_versions AS ( + SELECT dv.id FROM document_versions dv + INNER JOIN source_version sv ON dv.document_id = sv.document_id AND dv.major = sv.major +) SELECT - id, - organization_id, - document_version_id, - state, - signed_by_profile_id, - signed_at, - requested_at, - created_at, - updated_at + document_version_signatures.id, + document_version_signatures.organization_id, + document_version_signatures.document_version_id, + document_version_signatures.state, + document_version_signatures.signed_by_profile_id, + document_version_signatures.signed_at, + document_version_signatures.requested_at, + document_version_signatures.created_at, + document_version_signatures.updated_at FROM document_version_signatures +INNER JOIN major_versions mv ON document_version_signatures.document_version_id = mv.id WHERE %s - AND document_version_id = @document_version_id AND %s AND %s ` @@ -348,7 +355,14 @@ func (pvss *DocumentVersionSignaturesWithPeople) LoadByDocumentVersionIDWithPeop limit int, ) error { q := ` -WITH sigs AS ( +WITH source_version AS ( + SELECT document_id, major FROM document_versions WHERE id = @document_version_id +), +major_versions AS ( + SELECT dv.id FROM document_versions dv + INNER JOIN source_version sv ON dv.document_id = sv.document_id AND dv.major = sv.major +), +signatures_with_people AS ( SELECT dvs.id, dvs.organization_id, @@ -360,16 +374,10 @@ WITH sigs AS ( dvs.requested_at, dvs.created_at, dvs.updated_at, - p.full_name as signed_by_full_name - FROM - document_version_signatures dvs - INNER JOIN - iam_membership_profiles p ON dvs.signed_by_profile_id = p.id - WHERE - dvs.document_version_id = @document_version_id - ORDER BY - p.full_name ASC - LIMIT @limit + p.full_name AS signed_by_full_name + FROM document_version_signatures dvs + INNER JOIN major_versions mv ON dvs.document_version_id = mv.id + INNER JOIN iam_membership_profiles p ON dvs.signed_by_profile_id = p.id ) SELECT id, @@ -383,9 +391,12 @@ SELECT updated_at, signed_by_full_name FROM - sigs + signatures_with_people WHERE %s +ORDER BY + signed_by_full_name ASC +LIMIT @limit ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -419,18 +430,31 @@ func (pvs *DocumentVersionSignature) IsSignedByUserEmail( userEmail mail.Addr, ) (bool, error) { q := ` -SELECT EXISTS ( - SELECT 1 +WITH source_version AS ( + SELECT document_id, major FROM document_versions WHERE id = @document_version_id +), +major_versions AS ( + SELECT dv.id FROM document_versions dv + INNER JOIN source_version sv ON dv.document_id = sv.document_id AND dv.major = sv.major +), +signed_emails AS ( + SELECT dvs.id, dvs.tenant_id FROM document_version_signatures dvs + INNER JOIN major_versions mv ON dvs.document_version_id = mv.id INNER JOIN iam_membership_profiles p ON dvs.signed_by_profile_id = p.id INNER JOIN identities i ON p.identity_id = i.id - WHERE dvs.document_version_id = @document_version_id - AND i.email_address = @user_email::CITEXT + WHERE i.email_address = @user_email::CITEXT AND dvs.state = 'SIGNED' - AND dvs.tenant_id = @tenant_id +) +SELECT EXISTS ( + SELECT 1 + FROM signed_emails + WHERE %s ) AS signed ` + q = fmt.Sprintf(q, scope.SQLFragment()) + args := pgx.StrictNamedArgs{ "document_version_id": documentVersionID, "user_email": userEmail, @@ -458,13 +482,20 @@ func (dvs *DocumentVersionSignatures) CountByDocumentVersionID( filter *DocumentVersionSignatureFilter, ) (int, error) { q := ` +WITH source_version AS ( + SELECT document_id, major FROM document_versions WHERE id = @document_version_id +), +major_versions AS ( + SELECT dv.id FROM document_versions dv + INNER JOIN source_version sv ON dv.document_id = sv.document_id AND dv.major = sv.major +) SELECT - COUNT(id) + COUNT(document_version_signatures.id) FROM document_version_signatures +INNER JOIN major_versions mv ON document_version_signatures.document_version_id = mv.id WHERE %s - AND document_version_id = @document_version_id AND %s ` diff --git a/pkg/coredata/migrations/20260326T120000Z.sql b/pkg/coredata/migrations/20260326T120000Z.sql new file mode 100644 index 000000000..60b3e551c --- /dev/null +++ b/pkg/coredata/migrations/20260326T120000Z.sql @@ -0,0 +1,11 @@ +ALTER TABLE document_versions DROP CONSTRAINT document_versions_document_id_version_number_key; + +ALTER TABLE document_versions RENAME COLUMN version_number TO major; +ALTER TABLE document_versions ADD COLUMN minor INTEGER NOT NULL DEFAULT 0; +ALTER TABLE document_versions ALTER COLUMN minor DROP DEFAULT; + +ALTER TABLE document_versions ADD CONSTRAINT document_versions_document_id_major_minor_key UNIQUE (document_id, major, minor); + +ALTER TABLE documents RENAME COLUMN current_published_version TO current_published_major; +ALTER TABLE documents ADD COLUMN current_published_minor INTEGER; +UPDATE documents SET current_published_minor = 0 WHERE current_published_major IS NOT NULL; diff --git a/pkg/docgen/generator.go b/pkg/docgen/generator.go index f8a5d1eac..c2e5516c1 100644 --- a/pkg/docgen/generator.go +++ b/pkg/docgen/generator.go @@ -218,7 +218,8 @@ type ( DocumentData struct { Title string Content string - Version int + Major int + Minor int Classification Classification Approvers []string Description string diff --git a/pkg/docgen/generator_test.go b/pkg/docgen/generator_test.go index 0d3363fa8..c029829b9 100644 --- a/pkg/docgen/generator_test.go +++ b/pkg/docgen/generator_test.go @@ -39,7 +39,7 @@ func TestRenderHTML(t *testing.T) { data: DocumentData{ Title: "Test Document", Content: "# Main Title\n\nThis is **bold** text with *italic* formatting.", - Version: 1, + Major: 1, Classification: ClassificationPublic, Approvers: []string{"John Doe"}, PublishedAt: &now, @@ -57,7 +57,7 @@ func TestRenderHTML(t *testing.T) { "

Main Title

", "bold", "italic", - "1", + "1.0", "PUBLIC", "John Doe", "Alice Smith", @@ -383,7 +383,7 @@ func BenchmarkGenerateHTML(b *testing.B) { data := DocumentData{ Title: "Benchmark Document", Content: "# Title\n\nThis is **bold** text with *italic* formatting.\n\n- Item 1\n- Item 2", - Version: 1, + Major: 1, Classification: ClassificationPublic, Approvers: []string{"John Doe"}, PublishedAt: &now, diff --git a/pkg/docgen/template.html b/pkg/docgen/template.html index 34fe31621..3aef8cfac 100644 --- a/pkg/docgen/template.html +++ b/pkg/docgen/template.html @@ -404,7 +404,7 @@ {{- end}} Version: - {{.Version}} + {{.Major}}.{{.Minor}} {{- if .PublishedAt}} diff --git a/pkg/probo/document_approval_service.go b/pkg/probo/document_approval_service.go index a9a1c9536..49e3d3029 100644 --- a/pkg/probo/document_approval_service.go +++ b/pkg/probo/document_approval_service.go @@ -960,26 +960,13 @@ func (s *DocumentApprovalService) publishVersion( return fmt.Errorf("cannot load document version: %w", err) } - document := &coredata.Document{} - if err := document.LoadByID(ctx, tx, s.svc.scope, version.DocumentID); err != nil { - return fmt.Errorf("cannot load document: %w", err) - } + _, _, err := s.svc.Documents.publishMajorVersionInTx( + ctx, + tx, + version.DocumentID, + nil, + false, + ) - now := time.Now() - document.CurrentPublishedVersion = &version.VersionNumber - document.UpdatedAt = now - - version.Status = coredata.DocumentVersionStatusPublished - version.PublishedAt = &now - version.UpdatedAt = now - - if err := document.Update(ctx, tx, s.svc.scope); err != nil { - return fmt.Errorf("cannot update document: %w", err) - } - - if err := version.Update(ctx, tx, s.svc.scope); err != nil { - return fmt.Errorf("cannot update document version: %w", err) - } - - return nil + return err } diff --git a/pkg/probo/document_service.go b/pkg/probo/document_service.go index 271d9d171..97b5349bd 100644 --- a/pkg/probo/document_service.go +++ b/pkg/probo/document_service.go @@ -107,7 +107,6 @@ type ( BulkPublishVersionsRequest struct { DocumentIDs []gid.GID - PublishedBy gid.GID Changelog string } ) @@ -337,11 +336,11 @@ func (s DocumentService) GenerateChangelog( return fmt.Errorf("cannot load document: %w", err) } - if document.CurrentPublishedVersion == nil { + if document.CurrentPublishedMajor == nil { initialVersionChangelog := "Initial version" changelog = &initialVersionChangelog } else { - if err := publishedVersion.LoadByDocumentIDAndVersionNumber(ctx, conn, s.svc.scope, documentID, *document.CurrentPublishedVersion); err != nil { + if err := publishedVersion.LoadByDocumentIDAndVersion(ctx, conn, s.svc.scope, documentID, *document.CurrentPublishedMajor, *document.CurrentPublishedMinor); err != nil { return fmt.Errorf("cannot load published version: %w", err) } } @@ -369,7 +368,7 @@ func (s DocumentService) GenerateChangelog( return changelog, nil } -func (s *DocumentService) BulkPublishVersions( +func (s *DocumentService) BulkPublishMajorVersions( ctx context.Context, req BulkPublishVersionsRequest, ) ([]*coredata.DocumentVersion, []*coredata.Document, error) { @@ -380,7 +379,7 @@ func (s *DocumentService) BulkPublishVersions( ctx, func(tx pg.Conn) error { for _, documentID := range req.DocumentIDs { - document, version, err := s.publishVersionInTx(ctx, tx, documentID, req.PublishedBy, &req.Changelog, true) + document, version, err := s.publishMajorVersionInTx(ctx, tx, documentID, &req.Changelog, true) if err != nil { return fmt.Errorf("cannot publish document %q: %w", documentID, err) } @@ -400,7 +399,38 @@ func (s *DocumentService) BulkPublishVersions( return publishedVersions, updatedDocuments, nil } -func (s *DocumentService) PublishVersion( +func (s *DocumentService) BulkPublishMinorVersions( + ctx context.Context, + req BulkPublishVersionsRequest, +) ([]*coredata.DocumentVersion, []*coredata.Document, error) { + var publishedVersions []*coredata.DocumentVersion + var updatedDocuments []*coredata.Document + + err := s.svc.pg.WithTx( + ctx, + func(tx pg.Conn) error { + for _, documentID := range req.DocumentIDs { + document, version, err := s.publishMinorVersionInTx(ctx, tx, documentID, &req.Changelog, true) + if err != nil { + return fmt.Errorf("cannot publish document %q: %w", documentID, err) + } + + publishedVersions = append(publishedVersions, version) + updatedDocuments = append(updatedDocuments, document) + } + + return nil + }, + ) + + if err != nil { + return nil, nil, err + } + + return publishedVersions, updatedDocuments, nil +} + +func (s *DocumentService) PublishMajorVersion( ctx context.Context, documentID gid.GID, publishedBy gid.GID, @@ -414,9 +444,9 @@ func (s *DocumentService) PublishVersion( func(tx pg.Conn) error { var err error - document, documentVersion, err = s.publishVersionInTx(ctx, tx, documentID, publishedBy, changelog, false) + document, documentVersion, err = s.publishMajorVersionInTx(ctx, tx, documentID, changelog, false) if err != nil { - return fmt.Errorf("cannot publish version: %w", err) + return fmt.Errorf("cannot publish major version: %w", err) } return nil @@ -430,66 +460,31 @@ func (s *DocumentService) PublishVersion( return document, documentVersion, nil } -func (s *DocumentService) publishVersionInTx( +func (s *DocumentService) PublishMinorVersion( ctx context.Context, - tx pg.Conn, documentID gid.GID, publishedBy gid.GID, changelog *string, - ignoreExisting bool, ) (*coredata.Document, *coredata.DocumentVersion, error) { - document := &coredata.Document{} - documentVersion := &coredata.DocumentVersion{} - publishedVersion := &coredata.DocumentVersion{} - now := time.Now() + var document *coredata.Document + var documentVersion *coredata.DocumentVersion - if err := document.LoadByID(ctx, tx, s.svc.scope, documentID); err != nil { - return nil, nil, fmt.Errorf("cannot load document %q: %w", documentID, err) - } + err := s.svc.pg.WithTx( + ctx, + func(tx pg.Conn) error { + var err error - if document.ArchivedAt != nil { - return nil, nil, &ErrDocumentArchived{} - } + document, documentVersion, err = s.publishMinorVersionInTx(ctx, tx, documentID, changelog, false) + if err != nil { + return fmt.Errorf("cannot publish minor version: %w", err) + } - if err := documentVersion.LoadLatestVersion(ctx, tx, s.svc.scope, documentID); err != nil { - return nil, nil, fmt.Errorf("cannot load current draft: %w", err) - } + return nil + }, + ) - if ignoreExisting && documentVersion.Status == coredata.DocumentVersionStatusPublished { - return document, documentVersion, nil - } - - if documentVersion.Status != coredata.DocumentVersionStatusDraft { - return nil, nil, fmt.Errorf("cannot publish version") - } - - if document.CurrentPublishedVersion != nil { - if err := publishedVersion.LoadByDocumentIDAndVersionNumber(ctx, tx, s.svc.scope, documentID, *document.CurrentPublishedVersion); err != nil { - return nil, nil, fmt.Errorf("cannot load published version: %w", err) - } - if publishedVersion.Content == documentVersion.Content && - publishedVersion.Title == documentVersion.Title { - return nil, nil, &ErrDocumentVersionNoChanges{} - } - } - - if changelog != nil { - documentVersion.Changelog = *changelog - } - - document.CurrentPublishedVersion = &documentVersion.VersionNumber - document.UpdatedAt = now - - documentVersion.Status = coredata.DocumentVersionStatusPublished - documentVersion.PublishedAt = &now - documentVersion.UpdatedAt = now - - if err := document.Update(ctx, tx, s.svc.scope); err != nil { - return nil, nil, fmt.Errorf("cannot update document: %w", err) - } - - if err := documentVersion.Update(ctx, tx, s.svc.scope); err != nil { - return nil, nil, fmt.Errorf("cannot update document version: %w", err) + if err != nil { + return nil, nil, err } return document, documentVersion, nil @@ -528,7 +523,8 @@ func (s *DocumentService) Create( ID: documentVersionID, DocumentID: documentID, Title: req.Title, - VersionNumber: 1, + Major: 0, + Minor: 1, Content: req.Content, Status: coredata.DocumentVersionStatusDraft, Classification: req.Classification, @@ -1004,7 +1000,8 @@ func (s *DocumentService) CreateDraft( draftVersion.OrganizationID = document.OrganizationID draftVersion.DocumentID = documentID draftVersion.Title = document.Title - draftVersion.VersionNumber = latestVersion.VersionNumber + 1 + draftVersion.Major = latestVersion.Major + draftVersion.Minor = latestVersion.Minor + 1 draftVersion.Classification = document.Classification draftVersion.Content = latestVersion.Content draftVersion.Status = coredata.DocumentVersionStatusDraft @@ -1043,7 +1040,7 @@ func (s *DocumentService) DeleteDraft( return fmt.Errorf("cannot delete published document version") } - if documentVersion.VersionNumber == 1 { + if documentVersion.Major == 0 && documentVersion.Minor == 1 { return fmt.Errorf("cannot delete the first version of a document") } @@ -1961,7 +1958,8 @@ func exportDocumentPDF( docData := docgen.DocumentData{ Title: version.Title, Content: version.Content, - Version: version.VersionNumber, + Major: version.Major, + Minor: version.Minor, Classification: classification, Approvers: approverNames, PublishedAt: version.PublishedAt, @@ -2168,3 +2166,133 @@ func sanitizeFilename(title string) string { return sanitized } + +func (s *DocumentService) loadDraftForPublish( + ctx context.Context, + tx pg.Conn, + documentID gid.GID, + ignoreExisting bool, +) (*coredata.Document, *coredata.DocumentVersion, error) { + document := &coredata.Document{} + documentVersion := &coredata.DocumentVersion{} + + if err := document.LoadByID(ctx, tx, s.svc.scope, documentID); err != nil { + 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.DocumentVersionStatusPublished { + return document, documentVersion, nil + } + + if documentVersion.Status != coredata.DocumentVersionStatusDraft { + return nil, nil, &ErrDocumentVersionNotDraft{} + } + + return document, documentVersion, nil +} + +func (s *DocumentService) finalizePublish( + ctx context.Context, + tx pg.Conn, + document *coredata.Document, + documentVersion *coredata.DocumentVersion, + changelog *string, +) error { + now := time.Now() + + if changelog != nil { + documentVersion.Changelog = *changelog + } + + document.UpdatedAt = now + documentVersion.Status = coredata.DocumentVersionStatusPublished + documentVersion.PublishedAt = &now + documentVersion.UpdatedAt = now + + if err := document.Update(ctx, tx, s.svc.scope); err != nil { + return fmt.Errorf("cannot update document: %w", err) + } + + if err := documentVersion.Update(ctx, tx, s.svc.scope); err != nil { + return fmt.Errorf("cannot update document version: %w", err) + } + + return nil +} + +func (s *DocumentService) publishMajorVersionInTx( + ctx context.Context, + tx pg.Conn, + documentID gid.GID, + changelog *string, + ignoreExisting bool, +) (*coredata.Document, *coredata.DocumentVersion, error) { + document, documentVersion, err := s.loadDraftForPublish(ctx, tx, documentID, ignoreExisting) + if err != nil { + return nil, nil, err + } + + if ignoreExisting && documentVersion.Status == coredata.DocumentVersionStatusPublished { + return document, documentVersion, nil + } + + if document.CurrentPublishedMajor != nil { + publishedVersion := &coredata.DocumentVersion{} + if err := publishedVersion.LoadByDocumentIDAndVersion(ctx, tx, s.svc.scope, documentID, *document.CurrentPublishedMajor, *document.CurrentPublishedMinor); err != nil { + return nil, nil, fmt.Errorf("cannot load published version: %w", err) + } + + if publishedVersion.Content == documentVersion.Content && + publishedVersion.Title == documentVersion.Title { + return nil, nil, &ErrDocumentVersionNoChanges{} + } + + documentVersion.Major = *document.CurrentPublishedMajor + 1 + } else { + documentVersion.Major = 1 + } + + documentVersion.Minor = 0 + document.CurrentPublishedMajor = &documentVersion.Major + document.CurrentPublishedMinor = &documentVersion.Minor + + if err := s.finalizePublish(ctx, tx, document, documentVersion, changelog); err != nil { + return nil, nil, err + } + + return document, documentVersion, nil +} + +func (s *DocumentService) publishMinorVersionInTx( + ctx context.Context, + tx pg.Conn, + documentID gid.GID, + changelog *string, + ignoreExisting bool, +) (*coredata.Document, *coredata.DocumentVersion, error) { + document, documentVersion, err := s.loadDraftForPublish(ctx, tx, documentID, ignoreExisting) + if err != nil { + return nil, nil, err + } + + if ignoreExisting && documentVersion.Status == coredata.DocumentVersionStatusPublished { + return document, documentVersion, nil + } + + document.CurrentPublishedMajor = &documentVersion.Major + document.CurrentPublishedMinor = &documentVersion.Minor + + if err := s.finalizePublish(ctx, tx, document, documentVersion, changelog); err != nil { + return nil, nil, err + } + + return document, documentVersion, nil +} diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index ff87919f7..afe01c8d8 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -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 } diff --git a/pkg/server/api/console/v1/types/document.go b/pkg/server/api/console/v1/types/document.go index 4e349f51a..30ca61e70 100644 --- a/pkg/server/api/console/v1/types/document.go +++ b/pkg/server/api/console/v1/types/document.go @@ -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, } } diff --git a/pkg/server/api/console/v1/types/document_version.go b/pkg/server/api/console/v1/types/document_version.go index 5825dc504..c00cdd28e 100644 --- a/pkg/server/api/console/v1/types/document_version.go +++ b/pkg/server/api/console/v1/types/document_version.go @@ -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, diff --git a/pkg/server/api/console/v1/types/employee_document.go b/pkg/server/api/console/v1/types/employee_document.go index 1ba5da99e..72d25b811 100644 --- a/pkg/server/api/console/v1/types/employee_document.go +++ b/pkg/server/api/console/v1/types/employee_document.go @@ -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 diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 58f00d16e..92ac96b54 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -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) } diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go index 542ed0588..6b8749bdc 100644 --- a/pkg/server/api/mcp/v1/schema.resolvers.go +++ b/pkg/server/api/mcp/v1/schema.resolvers.go @@ -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 +} diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml index f6c9fabca..e8ce19ee1 100644 --- a/pkg/server/api/mcp/v1/specification.yaml +++ b/pkg/server/api/mcp/v1/specification.yaml @@ -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 diff --git a/pkg/server/api/mcp/v1/types/document.go b/pkg/server/api/mcp/v1/types/document.go index b3066bd55..8e29bbcac 100644 --- a/pkg/server/api/mcp/v1/types/document.go +++ b/pkg/server/api/mcp/v1/types/document.go @@ -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, diff --git a/pkg/trust/document_service.go b/pkg/trust/document_service.go index f949b1264..e874a094c 100644 --- a/pkg/trust/document_service.go +++ b/pkg/trust/document_service.go @@ -244,7 +244,8 @@ func (s *DocumentService) exportPDFData( docData := docgen.DocumentData{ Title: version.Title, Content: version.Content, - Version: version.VersionNumber, + Major: version.Major, + Minor: version.Minor, Classification: classification, Approvers: approverNames, PublishedAt: version.PublishedAt,