Move document type from document to document version

Follow the same pattern used for classification: document type now lives
exclusively on DocumentVersion. A migration copies existing values from
documents to their versions. The document filter uses a subquery on the
latest version. All three API surfaces (GraphQL, MCP, CLI), resolvers,
frontend, and e2e tests are updated accordingly.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-03-31 19:57:52 +02:00
parent 28cf3f167a
commit 9a418a7711
29 changed files with 396 additions and 314 deletions

View File

@@ -44,11 +44,11 @@ const linkedDocumentFragment = graphql`
fragment LinkedDocumentsCardFragment on Document {
id
title
documentType
versions(first: 1) {
edges {
node {
id
documentType
status
}
}
@@ -221,7 +221,7 @@ function DocumentRow(props: {
</div>
</Td>
<Td>
<DocumentTypeBadge type={document.documentType} />
<DocumentTypeBadge type={document.versions.edges[0].node.documentType} />
</Td>
<Td>
<DocumentVersionBadge state={document.versions.edges[0].node.status} />

View File

@@ -72,7 +72,13 @@ const documentsFragment = graphql`
node {
id
title
documentType
versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
edges {
node {
documentType
}
}
}
}
}
}
@@ -187,7 +193,7 @@ function DocumentRow(props: RowProps) {
onClick={() => onClick(props.document.id)}
>
{props.document.title}
<DocumentTypeBadge type={props.document.documentType} />
<DocumentTypeBadge type={props.document.versions.edges[0].node.documentType} />
<Button
disabled={props.disabled}
className="ml-auto"

View File

@@ -13,10 +13,7 @@
// PERFORMANCE OF THIS SOFTWARE.
import type { TrustCenterDocumentAccessStatus } from "@probo/coredata";
import {
getTrustCenterDocumentAccessInfo,
type TrustCenterDocumentAccessInfo,
} from "@probo/helpers";
import type { TrustCenterDocumentAccessInfo } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import {
Button,
@@ -31,8 +28,9 @@ import {
usePreloadedQuery,
useQueryLoader,
} from "react-relay";
import { graphql } from "relay-runtime";
import { graphql, readInlineData } from "relay-runtime";
import type { CompliancePageAccessEditDialogDocumentAccessFragment$data, CompliancePageAccessEditDialogDocumentAccessFragment$key } from "#/__generated__/core/CompliancePageAccessEditDialogDocumentAccessFragment.graphql";
import type { CompliancePageAccessEditDialogQuery as CompliancePageAccessEditDialogQueryType } from "#/__generated__/core/CompliancePageAccessEditDialogQuery.graphql";
import type { CompliancePageAccessEditDialogUpdateMutation } from "#/__generated__/core/CompliancePageAccessEditDialogUpdateMutation.graphql";
import type { CompliancePageAccessListItemFragment$data } from "#/__generated__/core/CompliancePageAccessListItemFragment.graphql";
@@ -40,6 +38,90 @@ import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { CompliancePageDocumentAccessList } from "#/pages/organizations/compliance-page/access/_components/CompliancePageDocumentAccessList";
import { ElectronicSignatureSection } from "#/pages/organizations/compliance-page/access/_components/ElectronicSignatureSection";
const documentAccessFragment = graphql`
fragment CompliancePageAccessEditDialogDocumentAccessFragment on TrustCenterDocumentAccess @inline {
id
status
document {
id
title
versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
edges {
node {
documentType
}
}
}
}
report {
id
filename
audit {
id
framework {
name
}
}
}
trustCenterFile {
id
name
category
}
}
`;
function getTrustCenterDocumentAccessInfo(
fragmentRef: CompliancePageAccessEditDialogDocumentAccessFragment$key,
__: (key: string) => string,
): TrustCenterDocumentAccessInfo {
const node = readInlineData(documentAccessFragment, fragmentRef);
return toDocumentAccessInfo(node, __);
}
function toDocumentAccessInfo(
node: CompliancePageAccessEditDialogDocumentAccessFragment$data,
__: (key: string) => string,
): TrustCenterDocumentAccessInfo {
if (node.document) {
return {
persisted: node.id !== node.document.id,
variant: "info",
name: node.document.title,
type: "document",
typeLabel: __("Document"),
category: node.document.versions?.edges[0]?.node.documentType ?? "",
id: node.document.id,
status: node.status,
};
}
if (node.report) {
return {
persisted: node.id !== node.report.id,
variant: "success",
name: node.report.filename,
type: "report",
typeLabel: __("Report"),
category: node.report.audit?.framework?.name ?? "",
id: node.report.id,
status: node.status,
};
}
if (node.trustCenterFile) {
return {
persisted: node.id !== node.trustCenterFile.id,
variant: "highlight",
name: node.trustCenterFile.name,
type: "file",
typeLabel: __("File"),
category: node.trustCenterFile.category,
id: node.trustCenterFile.id,
status: node.status,
};
}
throw new Error("Unknown trust center access document type");
}
const compliancePageAccessEditDialogQuery = graphql`
query CompliancePageAccessEditDialogQuery($accessId: ID!) {
node(id: $accessId) {
@@ -54,38 +136,7 @@ const compliancePageAccessEditDialogQuery = graphql`
) {
edges {
node {
id
status
# eslint-disable-next-line relay/unused-fields
document {
id
# eslint-disable-next-line relay/unused-fields
title
# eslint-disable-next-line relay/unused-fields
documentType
}
# eslint-disable-next-line relay/unused-fields
report {
id
# eslint-disable-next-line relay/unused-fields
filename
# eslint-disable-next-line relay/unused-fields
audit {
id
# eslint-disable-next-line relay/unused-fields
framework {
name
}
}
}
# eslint-disable-next-line relay/unused-fields
trustCenterFile {
id
# eslint-disable-next-line relay/unused-fields
name
# eslint-disable-next-line relay/unused-fields
category
}
...CompliancePageAccessEditDialogDocumentAccessFragment
}
}
}

View File

@@ -33,7 +33,6 @@ const compliancePageFragment = graphql`
const documentFragment = graphql`
fragment CompliancePageDocumentListItem_documentFragment on Document {
id
documentType
trustCenterVisibility
latestPublishedVersion: versions(
first: 1
@@ -43,6 +42,7 @@ const documentFragment = graphql`
edges {
node {
title
documentType
}
}
}
@@ -102,7 +102,8 @@ export function CompliancePageDocumentListItem(props: {
[document.id, updateDocumentVisibility],
);
const versionTitle = document.latestPublishedVersion.edges[0]?.node.title;
const latestVersion = document.latestPublishedVersion.edges[0]?.node;
const versionTitle = latestVersion?.title;
return (
<Tr to={`/organizations/${organizationId}/documents/${document.id}`}>
@@ -110,7 +111,7 @@ export function CompliancePageDocumentListItem(props: {
<div className="flex gap-4 items-center">{versionTitle}</div>
</Td>
<Td>
<DocumentTypeBadge type={document.documentType} />
{latestVersion && <DocumentTypeBadge type={latestVersion.documentType} />}
</Td>
<Td noLink width={130} className="pr-0">
<Field

View File

@@ -14,7 +14,7 @@
import { documentClassifications, documentTypes, formatDate, getDocumentClassificationLabel, getDocumentTypeLabel } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Badge, Button, Drawer, IconCheckmark1, IconCrossLargeX, IconPencil, PropertyRow } from "@probo/ui";
import { Badge, Button, Drawer, IconCheckmark1, IconCrossLargeX, IconPencil, PropertyRow, useToast } from "@probo/ui";
import { useState } from "react";
import { useFragment, useMutation } from "react-relay";
import { graphql } from "relay-runtime";
@@ -31,7 +31,6 @@ import { useFormWithSchema } from "#/hooks/useFormWithSchema";
const documentFragment = graphql`
fragment DocumentLayoutDrawer_documentFragment on Document {
id
documentType
status
archivedAt
canUpdate: permission(action: "core:document:update")
@@ -41,6 +40,7 @@ const documentFragment = graphql`
const versionFragment = graphql`
fragment DocumentLayoutDrawer_versionFragment on DocumentVersion {
id
documentType
classification
major
minor
@@ -50,10 +50,10 @@ const versionFragment = graphql`
}
`;
const updateDocumentMutation = graphql`
mutation DocumentLayoutDrawerMutation($input: UpdateDocumentInput!) {
updateDocument(input: $input) {
document {
const updateDocumentTypeMutation = graphql`
mutation DocumentLayoutDrawerMutation($input: UpdateDocumentVersionInput!) {
updateDocumentVersion(input: $input) {
documentVersion {
id
documentType
}
@@ -91,6 +91,7 @@ export function DocumentLayoutDrawer(props: {
const [isEditingType, setIsEditingType] = useState(false);
const [isEditingClassification, setIsEditingClassification] = useState(false);
const { toast } = useToast();
const document = useFragment<DocumentLayoutDrawer_documentFragment$key>(documentFragment, documentFragmentRef);
const version = useFragment<DocumentLayoutDrawer_versionFragment$key>(versionFragment, versionFragmentRef);
@@ -101,7 +102,7 @@ export function DocumentLayoutDrawer(props: {
schema,
{
defaultValues: {
documentType: document.documentType,
documentType: version.documentType,
},
},
);
@@ -119,8 +120,8 @@ export function DocumentLayoutDrawer(props: {
},
);
const [updateDocument, isUpdatingDocument]
= useMutation<DocumentLayoutDrawerMutation>(updateDocumentMutation);
const [updateDocumentType, isUpdatingDocumentType]
= useMutation<DocumentLayoutDrawerMutation>(updateDocumentTypeMutation);
const [updateClassification, isUpdatingClassification]
= useMutation<DocumentLayoutDrawer_updateClassificationMutation>(updateClassificationMutation);
@@ -128,15 +129,27 @@ export function DocumentLayoutDrawer(props: {
const handleUpdateDocumentType = (data: {
documentType: (typeof documentTypes)[number];
}) => {
updateDocument({
updateDocumentType({
variables: {
input: {
id: document.id,
documentVersionId: version.id,
documentType: data.documentType,
},
},
onCompleted: () => {
setIsEditingType(false);
toast({
title: __("Success"),
description: __("Document type updated successfully"),
variant: "success",
});
},
onError: () => {
toast({
title: __("Error"),
description: __("Failed to update document type"),
variant: "error",
});
},
});
};
@@ -153,6 +166,18 @@ export function DocumentLayoutDrawer(props: {
},
onCompleted: () => {
setIsEditingClassification(false);
toast({
title: __("Success"),
description: __("Document classification updated successfully"),
variant: "success",
});
},
onError: () => {
toast({
title: __("Error"),
description: __("Failed to update document classification"),
variant: "error",
});
},
});
};
@@ -171,7 +196,7 @@ export function DocumentLayoutDrawer(props: {
setIsEditingType(false);
reset();
}}
disabled={isUpdatingDocument}
disabled={isUpdatingDocumentType}
>
<ControlledField
name="documentType"
@@ -185,10 +210,10 @@ export function DocumentLayoutDrawer(props: {
: (
<ReadOnlyPropertyContent
onEdit={() => setIsEditingType(true)}
canEdit={canEdit}
canEdit={canEdit && isDraft}
>
<div className="text-sm text-txt-secondary">
{getDocumentTypeLabel(__, document.documentType)}
{getDocumentTypeLabel(__, version.documentType)}
</div>
</ReadOnlyPropertyContent>
)}

View File

@@ -26,7 +26,6 @@ const fragment = graphql`
fragment DocumentListItemFragment on Document {
id
title
documentType
updatedAt
canDelete: permission(action: "core:document:delete")
recentVersions: versions(first: 2 orderBy: { field: CREATED_AT direction: DESC }) {
@@ -36,6 +35,7 @@ const fragment = graphql`
status
major
minor
documentType
classification
approvalQuorums(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
edges {
@@ -165,7 +165,7 @@ export function DocumentListItem(props: {
{lastVersion.minor}
</Td>
<Td className="w-28">
{getDocumentTypeLabel(__, document.documentType)}
{getDocumentTypeLabel(__, lastVersion.documentType)}
</Td>
<Td className="w-32">
{getDocumentClassificationLabel(__, lastVersion.classification)}

View File

@@ -27,12 +27,12 @@ const fragment = graphql`
fragment ApprovableDocumentRowFragment on EmployeeDocument {
id
title
documentType
approvalState
updatedAt
lastVersion: versions(first: 1 orderBy: { field: CREATED_AT direction: DESC }) {
edges {
node {
documentType
classification
}
}
@@ -67,7 +67,7 @@ export function ApprovableDocumentRow({
<Tr to={`/organizations/${organizationId}/employee/approvals/${document.id}`}>
<Td>{document.title}</Td>
<Td className="w-48">
{getDocumentTypeLabel(__, document.documentType)}
{getDocumentTypeLabel(__, lastVersion.documentType)}
</Td>
<Td className="w-36">
<Badge variant="neutral">

View File

@@ -27,12 +27,12 @@ const fragment = graphql`
fragment DocumentRowFragment on EmployeeDocument {
id
title
documentType
signed
updatedAt
lastVersion: versions(first: 1 orderBy: { field: CREATED_AT direction: DESC }) {
edges {
node {
documentType
classification
}
}
@@ -55,7 +55,7 @@ export function DocumentRow({
<Tr to={`/organizations/${organizationId}/employee/signatures/${document.id}`}>
<Td>{document.title}</Td>
<Td className="w-48">
{getDocumentTypeLabel(__, document.documentType)}
{getDocumentTypeLabel(__, lastVersion.documentType)}
</Td>
<Td className="w-36">
<Badge variant="neutral">

View File

@@ -64,8 +64,8 @@ const overviewFragment = graphql`
edges {
node {
id
...DocumentRowFragment
documentType
...DocumentRowFragment
}
}
}

View File

@@ -103,6 +103,11 @@ func TestDocument_Create(t *testing.T) {
node {
id
title
}
}
documentVersionEdge {
node {
id
documentType
}
}
@@ -119,11 +124,16 @@ func TestDocument_Create(t *testing.T) {
CreateDocument struct {
DocumentEdge struct {
Node struct {
ID string `json:"id"`
Title string `json:"title"`
DocumentType string `json:"documentType"`
ID string `json:"id"`
Title string `json:"title"`
} `json:"node"`
} `json:"documentEdge"`
DocumentVersionEdge struct {
Node struct {
ID string `json:"id"`
DocumentType string `json:"documentType"`
} `json:"node"`
} `json:"documentVersionEdge"`
} `json:"createDocument"`
}
@@ -133,11 +143,13 @@ func TestDocument_Create(t *testing.T) {
node := result.CreateDocument.DocumentEdge.Node
assert.NotEmpty(t, node.ID)
versionNode := result.CreateDocument.DocumentVersionEdge.Node
switch tt.assertField {
case "title":
assert.Equal(t, tt.assertValue, node.Title)
case "documentType":
assert.Equal(t, tt.assertValue, node.DocumentType)
assert.Equal(t, tt.assertValue, versionNode.DocumentType)
}
})
}
@@ -277,48 +289,14 @@ func TestDocument_Update(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
tests := []struct {
name string
setup func() string
input func(id string) map[string]any
assertField string
assertValue string
}{
{
name: "update title",
setup: func() string {
return factory.NewDocument(owner).
WithTitle("Document to Update").
Create()
},
input: func(id string) map[string]any {
return map[string]any{
"id": id,
"title": "Updated Document Title",
}
},
assertField: "title",
assertValue: "Updated Document Title",
},
{
name: "update document type",
setup: func() string {
return factory.NewDocument(owner).
WithTitle("Type Test").
WithDocumentType("POLICY").
Create()
},
input: func(id string) map[string]any {
return map[string]any{"id": id, "documentType": "PROCEDURE"}
},
assertField: "documentType",
assertValue: "PROCEDURE",
},
}
t.Run(
"update title",
func(t *testing.T) {
t.Parallel()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
documentID := tt.setup()
documentID := factory.NewDocument(owner).
WithTitle("Document to Update").
Create()
query := `
mutation UpdateDocument($input: UpdateDocumentInput!) {
@@ -326,7 +304,6 @@ func TestDocument_Update(t *testing.T) {
document {
id
title
documentType
}
}
}
@@ -335,25 +312,22 @@ func TestDocument_Update(t *testing.T) {
var result struct {
UpdateDocument struct {
Document struct {
ID string `json:"id"`
Title string `json:"title"`
DocumentType string `json:"documentType"`
ID string `json:"id"`
Title string `json:"title"`
} `json:"document"`
} `json:"updateDocument"`
}
err := owner.Execute(query, map[string]any{"input": tt.input(documentID)}, &result)
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"id": documentID,
"title": "Updated Document Title",
},
}, &result)
require.NoError(t, err)
doc := result.UpdateDocument.Document
switch tt.assertField {
case "title":
assert.Equal(t, tt.assertValue, doc.Title)
case "documentType":
assert.Equal(t, tt.assertValue, doc.DocumentType)
}
})
}
assert.Equal(t, "Updated Document Title", result.UpdateDocument.Document.Title)
},
)
}
func TestDocument_Update_Validation(t *testing.T) {

View File

@@ -20,28 +20,3 @@ export const trustCenterDocumentAccessStatus = {
} as const;
export type TrustCenterDocumentAccessStatus = (typeof trustCenterDocumentAccessStatus)[keyof typeof trustCenterDocumentAccessStatus];
export type TrustCenterDocumentAccess = {
id: string;
status: TrustCenterDocumentAccessStatus;
document?: {
id: string;
title: string;
documentType: string;
} | null;
report?: {
id: string;
filename: string;
audit?: {
id: string;
framework?: {
name: string;
} | null;
} | null;
} | null;
trustCenterFile?: {
id: string;
name: string;
category: string;
} | null;
};

View File

@@ -13,6 +13,5 @@
// PERFORMANCE OF THIS SOFTWARE.
export type {
TrustCenterDocumentAccess,
TrustCenterDocumentAccessStatus,
} from "./TrustCenterDocumentAccess";

View File

@@ -107,7 +107,6 @@ export { detectSocialName } from "./socialUrl";
export { formatError, type GraphQLError } from "./error";
export { Role, roles, getAssignableRoles } from "./roles";
export {
getTrustCenterDocumentAccessInfo,
getTrustCenterDocumentAccessStatusBadgeVariant,
getTrustCenterDocumentAccessStatusLabel,
type TrustCenterDocumentAccessInfo,

View File

@@ -12,7 +12,7 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import type { TrustCenterDocumentAccess, TrustCenterDocumentAccessStatus } from "@probo/coredata";
import type { TrustCenterDocumentAccessStatus } from "@probo/coredata";
export function getTrustCenterDocumentAccessStatusBadgeVariant(status: TrustCenterDocumentAccessStatus) {
switch (status) {
@@ -63,46 +63,3 @@ export type TrustCenterDocumentAccessInfo = ITrustCenterDocumentAccessInfo & (
}
)
export function getTrustCenterDocumentAccessInfo(
docAccess: TrustCenterDocumentAccess,
__: (key: string) => string
): TrustCenterDocumentAccessInfo {
if (docAccess.document) {
return {
persisted: docAccess.id !== docAccess.document.id,
variant: "info" as const,
name: docAccess.document.title,
type: "document",
typeLabel: __("Document"),
category: docAccess.document.documentType,
id: docAccess.document.id,
status: docAccess.status,
};
}
if (docAccess.report) {
return {
persisted: docAccess.id !== docAccess.report.id,
variant: "success" as const,
name: docAccess.report.filename,
type: "report",
typeLabel: __("Report"),
category: docAccess.report.audit?.framework?.name ?? "",
id: docAccess.report.id,
status: docAccess.status,
};
}
if (docAccess.trustCenterFile) {
return {
persisted: docAccess.id !== docAccess.trustCenterFile.id,
variant: "highlight" as const,
name: docAccess.trustCenterFile.name,
type: "file",
typeLabel: __("File"),
category: docAccess.trustCenterFile.category,
id: docAccess.trustCenterFile.id,
status: docAccess.status,
};
}
throw new Error("Unknown trust center access document type");
}

View File

@@ -33,7 +33,6 @@ type (
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
Title string `db:"title"`
DocumentType DocumentType `db:"document_type"`
CurrentPublishedMajor *int `db:"current_published_major"`
CurrentPublishedMinor *int `db:"current_published_minor"`
TrustCenterVisibility TrustCenterVisibility `db:"trust_center_visibility"`
@@ -41,6 +40,9 @@ type (
ArchivedAt *time.Time `db:"archived_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
// ordering only
DocumentType DocumentType `db:"document_type"`
}
Documents []*Document
@@ -122,24 +124,30 @@ func (p *Document) LoadByID(
documentID gid.GID,
) error {
q := `
WITH latest_versions AS (
SELECT DISTINCT ON (document_id) document_id, document_type
FROM document_versions
ORDER BY document_id, major DESC, minor DESC
)
SELECT
id,
organization_id,
title,
document_type,
current_published_major,
current_published_minor,
trust_center_visibility,
status,
archived_at,
created_at,
updated_at
documents.id,
documents.organization_id,
documents.title,
documents.current_published_major,
documents.current_published_minor,
documents.trust_center_visibility,
documents.status,
documents.archived_at,
documents.created_at,
documents.updated_at,
COALESCE(lv.document_type, 'OTHER') AS document_type
FROM
documents
LEFT JOIN latest_versions lv ON lv.document_id = documents.id
WHERE
%s
AND deleted_at IS NULL
AND id = @document_id
AND documents.deleted_at IS NULL
AND documents.id = @document_id
LIMIT 1;
`
@@ -175,24 +183,30 @@ func (p *Document) LoadByIDWithFilter(
filter *DocumentFilter,
) error {
q := `
WITH latest_versions AS (
SELECT DISTINCT ON (document_id) document_id, document_type
FROM document_versions
ORDER BY document_id, major DESC, minor DESC
)
SELECT
id,
organization_id,
title,
document_type,
current_published_major,
current_published_minor,
trust_center_visibility,
status,
archived_at,
created_at,
updated_at
documents.id,
documents.organization_id,
documents.title,
documents.current_published_major,
documents.current_published_minor,
documents.trust_center_visibility,
documents.status,
documents.archived_at,
documents.created_at,
documents.updated_at,
COALESCE(lv.document_type, 'OTHER') AS document_type
FROM
documents
LEFT JOIN latest_versions lv ON lv.document_id = documents.id
WHERE
%s
AND deleted_at IS NULL
AND id = @document_id
AND documents.deleted_at IS NULL
AND documents.id = @document_id
AND %s
LIMIT 1;
`
@@ -229,24 +243,30 @@ func (p *Documents) LoadByIDs(
documentIDs []gid.GID,
) error {
q := `
WITH latest_versions AS (
SELECT DISTINCT ON (document_id) document_id, document_type
FROM document_versions
ORDER BY document_id, major DESC, minor DESC
)
SELECT
id,
organization_id,
title,
document_type,
current_published_major,
current_published_minor,
trust_center_visibility,
status,
archived_at,
created_at,
updated_at
documents.id,
documents.organization_id,
documents.title,
documents.current_published_major,
documents.current_published_minor,
documents.trust_center_visibility,
documents.status,
documents.archived_at,
documents.created_at,
documents.updated_at,
COALESCE(lv.document_type, 'OTHER') AS document_type
FROM
documents
LEFT JOIN latest_versions lv ON lv.document_id = documents.id
WHERE
%s
AND deleted_at IS NULL
AND id = ANY(@document_ids)
AND documents.deleted_at IS NULL
AND documents.id = ANY(@document_ids)
`
q = fmt.Sprintf(q, scope.SQLFragment())
@@ -312,24 +332,30 @@ func (p *Documents) LoadByOrganizationID(
filter *DocumentFilter,
) error {
q := `
WITH latest_versions AS (
SELECT DISTINCT ON (document_id) document_id, document_type
FROM document_versions
ORDER BY document_id, major DESC, minor DESC
)
SELECT
id,
organization_id,
title,
document_type,
current_published_major,
current_published_minor,
trust_center_visibility,
status,
archived_at,
created_at,
updated_at
documents.id,
documents.organization_id,
documents.title,
documents.current_published_major,
documents.current_published_minor,
documents.trust_center_visibility,
documents.status,
documents.archived_at,
documents.created_at,
documents.updated_at,
COALESCE(lv.document_type, 'OTHER') AS document_type
FROM
documents
LEFT JOIN latest_versions lv ON lv.document_id = documents.id
WHERE
%s
AND deleted_at IS NULL
AND organization_id = @organization_id
AND documents.deleted_at IS NULL
AND documents.organization_id = @organization_id
AND %s
AND %s
`
@@ -364,24 +390,30 @@ func (p *Documents) LoadAllByOrganizationID(
filter *DocumentFilter,
) error {
q := `
WITH latest_versions AS (
SELECT DISTINCT ON (document_id) document_id, document_type
FROM document_versions
ORDER BY document_id, major DESC, minor DESC
)
SELECT
id,
organization_id,
title,
document_type,
current_published_major,
current_published_minor,
trust_center_visibility,
status,
archived_at,
created_at,
updated_at
documents.id,
documents.organization_id,
documents.title,
documents.current_published_major,
documents.current_published_minor,
documents.trust_center_visibility,
documents.status,
documents.archived_at,
documents.created_at,
documents.updated_at,
COALESCE(lv.document_type, 'OTHER') AS document_type
FROM
documents
LEFT JOIN latest_versions lv ON lv.document_id = documents.id
WHERE
%s
AND deleted_at IS NULL
AND organization_id = @organization_id
AND documents.deleted_at IS NULL
AND documents.organization_id = @organization_id
AND %s
ORDER BY title ASC
`
@@ -416,7 +448,12 @@ func (p *Documents) LoadPublishedByOrganizationID(
filter *DocumentFilter,
) error {
q := `
WITH published_documents AS (
WITH latest_versions AS (
SELECT DISTINCT ON (document_id) document_id, document_type
FROM document_versions
ORDER BY document_id, major DESC, minor DESC
),
published_documents AS (
SELECT
d.*,
dv.title AS published_title
@@ -431,19 +468,20 @@ WITH published_documents AS (
AND d.organization_id = @organization_id
)
SELECT
id,
organization_id,
COALESCE(published_title, title) AS title,
document_type,
current_published_major,
current_published_minor,
trust_center_visibility,
status,
archived_at,
created_at,
updated_at
documents.id,
documents.organization_id,
COALESCE(documents.published_title, documents.title) AS title,
documents.current_published_major,
documents.current_published_minor,
documents.trust_center_visibility,
documents.status,
documents.archived_at,
documents.created_at,
documents.updated_at,
COALESCE(lv.document_type, 'OTHER') AS document_type
FROM
published_documents documents
LEFT JOIN latest_versions lv ON lv.document_id = documents.id
WHERE
%s
AND %s
@@ -483,7 +521,6 @@ INSERT INTO
id,
organization_id,
title,
document_type,
current_published_major,
current_published_minor,
trust_center_visibility,
@@ -497,7 +534,6 @@ VALUES (
@document_id,
@organization_id,
@title,
@document_type,
@current_published_major,
@current_published_minor,
@trust_center_visibility,
@@ -513,7 +549,6 @@ VALUES (
"document_id": p.ID,
"organization_id": p.OrganizationID,
"title": p.Title,
"document_type": p.DocumentType,
"current_published_major": p.CurrentPublishedMajor,
"current_published_minor": p.CurrentPublishedMinor,
"trust_center_visibility": p.TrustCenterVisibility,
@@ -575,7 +610,6 @@ SET
title = @title,
current_published_major = @current_published_major,
current_published_minor = @current_published_minor,
document_type = @document_type,
trust_center_visibility = @trust_center_visibility,
status = @status,
archived_at = @archived_at,
@@ -593,7 +627,6 @@ WHERE
"title": p.Title,
"current_published_major": p.CurrentPublishedMajor,
"current_published_minor": p.CurrentPublishedMinor,
"document_type": p.DocumentType,
"trust_center_visibility": p.TrustCenterVisibility,
"status": p.Status,
"archived_at": p.ArchivedAt,
@@ -653,7 +686,12 @@ func (p *Documents) LoadByControlID(
filter *DocumentFilter,
) error {
q := `
WITH scoped_documents AS (
WITH latest_versions AS (
SELECT DISTINCT ON (document_id) document_id, document_type
FROM document_versions
ORDER BY document_id, major DESC, minor DESC
),
scoped_documents AS (
SELECT *
FROM documents
WHERE %s
@@ -665,16 +703,17 @@ SELECT
scoped_documents.id,
scoped_documents.organization_id,
scoped_documents.title,
scoped_documents.document_type,
scoped_documents.current_published_major,
scoped_documents.current_published_minor,
scoped_documents.trust_center_visibility,
scoped_documents.status,
scoped_documents.archived_at,
scoped_documents.created_at,
scoped_documents.updated_at
scoped_documents.updated_at,
COALESCE(lv.document_type, 'OTHER') AS document_type
FROM scoped_documents
INNER JOIN controls_documents cp ON scoped_documents.id = cp.document_id
LEFT JOIN latest_versions lv ON lv.document_id = scoped_documents.id
WHERE cp.control_id = @control_id
`
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
@@ -744,7 +783,12 @@ func (p *Documents) LoadByRiskID(
filter *DocumentFilter,
) error {
q := `
WITH scoped_documents AS (
WITH latest_versions AS (
SELECT DISTINCT ON (document_id) document_id, document_type
FROM document_versions
ORDER BY document_id, major DESC, minor DESC
),
scoped_documents AS (
SELECT *
FROM documents
WHERE %s
@@ -756,16 +800,17 @@ SELECT
scoped_documents.id,
scoped_documents.organization_id,
scoped_documents.title,
scoped_documents.document_type,
scoped_documents.current_published_major,
scoped_documents.current_published_minor,
scoped_documents.trust_center_visibility,
scoped_documents.status,
scoped_documents.archived_at,
scoped_documents.created_at,
scoped_documents.updated_at
scoped_documents.updated_at,
COALESCE(lv.document_type, 'OTHER') AS document_type
FROM scoped_documents
INNER JOIN risks_documents rp ON scoped_documents.id = rp.document_id
LEFT JOIN latest_versions lv ON lv.document_id = scoped_documents.id
WHERE rp.risk_id = @risk_id
`
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())

View File

@@ -180,7 +180,13 @@ func (f *DocumentFilter) SQLFragment() string {
AND
CASE
WHEN @document_types::document_type[] IS NOT NULL THEN
document_type = ANY(@document_types::document_type[])
(
SELECT dv.document_type
FROM document_versions dv
WHERE dv.document_id = documents.id
ORDER BY dv.major DESC, dv.minor DESC
LIMIT 1
) = ANY(@document_types::document_type[])
ELSE TRUE
END
AND

View File

@@ -14,6 +14,8 @@
package coredata
import "fmt"
type (
DocumentOrderField string
)
@@ -25,7 +27,25 @@ const (
)
func (p DocumentOrderField) Column() string {
return string(p)
switch p {
case DocumentOrderFieldCreatedAt:
return "created_at"
case DocumentOrderFieldTitle:
return "title"
case DocumentOrderFieldDocumentType:
return "document_type"
}
panic(fmt.Sprintf("unsupported order by: %s", p))
}
func (p DocumentOrderField) IsValid() bool {
switch p {
case DocumentOrderFieldCreatedAt,
DocumentOrderFieldTitle,
DocumentOrderFieldDocumentType:
return true
}
return false
}
func (p DocumentOrderField) String() string {
@@ -38,5 +58,8 @@ func (p DocumentOrderField) MarshalText() ([]byte, error) {
func (p *DocumentOrderField) UnmarshalText(text []byte) error {
*p = DocumentOrderField(text)
if !p.IsValid() {
return fmt.Errorf("%s is not a valid DocumentOrderField", string(text))
}
return nil
}

View File

@@ -37,6 +37,7 @@ type (
Major int `db:"major"`
Minor int `db:"minor"`
Classification DocumentClassification `db:"classification"`
DocumentType DocumentType `db:"document_type"`
Content string `db:"content"`
Changelog string `db:"changelog"`
Status DocumentVersionStatus `db:"status"`
@@ -120,6 +121,7 @@ SELECT
major,
minor,
classification,
document_type,
content,
changelog,
status,
@@ -182,6 +184,7 @@ SELECT
major,
minor,
classification,
document_type,
content,
changelog,
status,
@@ -233,6 +236,7 @@ INSERT INTO document_versions (
major,
minor,
classification,
document_type,
content,
changelog,
status,
@@ -248,6 +252,7 @@ VALUES (
@major,
@minor,
@classification,
@document_type,
@content,
@changelog,
@status,
@@ -264,6 +269,7 @@ VALUES (
"major": dv.Major,
"minor": dv.Minor,
"classification": dv.Classification,
"document_type": dv.DocumentType,
"content": dv.Content,
"changelog": dv.Changelog,
"status": dv.Status,
@@ -304,6 +310,7 @@ SELECT
major,
minor,
classification,
document_type,
content,
changelog,
status,
@@ -359,6 +366,7 @@ SELECT
major,
minor,
classification,
document_type,
content,
changelog,
status,
@@ -410,6 +418,7 @@ SELECT
major,
minor,
classification,
document_type,
content,
changelog,
status,
@@ -463,6 +472,7 @@ UPDATE document_versions SET
content = @content,
published_at = @published_at,
classification = @classification,
document_type = @document_type,
updated_at = @updated_at
WHERE %s
AND id = @document_version_id
@@ -480,6 +490,7 @@ WHERE %s
"content": dv.Content,
"published_at": dv.PublishedAt,
"classification": dv.Classification,
"document_type": dv.DocumentType,
"updated_at": dv.UpdatedAt,
}
maps.Copy(args, scope.SQLArguments())

View File

@@ -0,0 +1,12 @@
-- Add document_type column to document_versions, copying from parent document.
ALTER TABLE document_versions ADD COLUMN document_type document_type NOT NULL DEFAULT 'OTHER';
UPDATE document_versions dv
SET document_type = d.document_type
FROM documents d
WHERE dv.document_id = d.id;
ALTER TABLE document_versions ALTER COLUMN document_type DROP DEFAULT;
-- TODO: drop the document_type column from documents.
ALTER TABLE documents ALTER COLUMN document_type SET DEFAULT 'OTHER';

View File

@@ -290,7 +290,7 @@ func (s *DocumentApprovalService) Approve(
tx,
&esign.CreateAndAcceptSignatureRequest{
OrganizationID: documentVersion.OrganizationID,
DocumentType: coredata.ElectronicSignatureDocumentTypeFromDocumentType(document.DocumentType),
DocumentType: coredata.ElectronicSignatureDocumentTypeFromDocumentType(documentVersion.DocumentType),
DocumentName: &document.Title,
FileID: fileRecord.ID,
SignerEmail: req.SignerEmail,

View File

@@ -86,7 +86,6 @@ type (
UpdateDocumentRequest struct {
DocumentID gid.GID
Title *string
DocumentType *coredata.DocumentType
TrustCenterVisibility *coredata.TrustCenterVisibility
}
@@ -94,6 +93,7 @@ type (
ID gid.GID
Content *string
Classification *coredata.DocumentClassification
DocumentType *coredata.DocumentType
}
RequestSignatureRequest struct {
@@ -139,7 +139,6 @@ func (udr *UpdateDocumentRequest) Validate() error {
v.Check(udr.DocumentID, "document_id", validator.Required(), validator.GID(coredata.DocumentEntityType))
v.Check(udr.Title, "title", validator.SafeTextNoNewLine(TitleMaxLength))
v.Check(udr.DocumentType, "document_type", validator.OneOfSlice(coredata.DocumentTypes()))
v.Check(udr.TrustCenterVisibility, "trust_center_visibility", validator.OneOfSlice(coredata.TrustCenterVisibilities()))
return v.Error()
@@ -156,6 +155,7 @@ func (udvr *UpdateDocumentVersionRequest) Validate() error {
validator.MaxLen(documentMaxLength),
validator.ProseMirrorDocumentContent(),
)
v.Check(udvr.DocumentType, "document_type", validator.OneOfSlice(coredata.DocumentTypes()))
return v.Error()
}
@@ -518,7 +518,6 @@ func (s *DocumentService) Create(
document := &coredata.Document{
ID: documentID,
Title: req.Title,
DocumentType: req.DocumentType,
TrustCenterVisibility: coredata.TrustCenterVisibilityNone,
Status: coredata.DocumentStatusActive,
CreatedAt: now,
@@ -547,6 +546,7 @@ func (s *DocumentService) Create(
Content: content,
Status: coredata.DocumentVersionStatusDraft,
Classification: req.Classification,
DocumentType: req.DocumentType,
CreatedAt: now,
UpdatedAt: now,
}
@@ -793,6 +793,9 @@ func (s *DocumentService) UpdateVersion(
if req.Classification != nil {
documentVersion.Classification = *req.Classification
}
if req.DocumentType != nil {
documentVersion.DocumentType = *req.DocumentType
}
documentVersion.UpdatedAt = time.Now()
if err := documentVersion.Update(ctx, conn, s.svc.scope); err != nil {
@@ -1031,6 +1034,7 @@ func (s *DocumentService) CreateDraft(
draftVersion.Major = latestVersion.Major
draftVersion.Minor = latestVersion.Minor + 1
draftVersion.Classification = latestVersion.Classification
draftVersion.DocumentType = latestVersion.DocumentType
draftVersion.Content = latestVersion.Content
draftVersion.Status = coredata.DocumentVersionStatusDraft
draftVersion.CreatedAt = now
@@ -1561,14 +1565,6 @@ func (s *DocumentService) Update(
document.Title = *req.Title
}
if req.DocumentType != nil {
document.DocumentType = *req.DocumentType
}
if req.DocumentType != nil {
document.DocumentType = *req.DocumentType
}
if req.TrustCenterVisibility != nil {
document.TrustCenterVisibility = *req.TrustCenterVisibility
}

View File

@@ -2543,7 +2543,6 @@ type Document implements Node {
id: ID!
title: String!
description: String
documentType: DocumentType!
currentPublishedMajor: Int
currentPublishedMinor: Int
trustCenterVisibility: TrustCenterVisibility!
@@ -2583,7 +2582,6 @@ type EmployeeDocument
id: ID!
title: String!
description: String
documentType: DocumentType!
signed: Boolean @goField(forceResolver: true)
approvalState: DocumentVersionApprovalDecisionState @goField(forceResolver: true)
@@ -2608,6 +2606,7 @@ type EmployeeDocumentVersion
minor: Int!
status: DocumentVersionStatus!
classification: DocumentClassification!
documentType: DocumentType!
signed: Boolean! @goField(forceResolver: true)
approvalDecision: DocumentVersionApprovalDecision @goField(forceResolver: true)
publishedAt: Datetime
@@ -4642,7 +4641,6 @@ input UpdateDocumentInput {
id: ID!
title: String
content: String
documentType: DocumentType
trustCenterVisibility: TrustCenterVisibility
}
@@ -5529,6 +5527,7 @@ type DocumentVersion implements Node {
changelog: String!
title: String!
classification: DocumentClassification!
documentType: DocumentType!
approvers(
first: Int
after: CursorKey
@@ -5900,6 +5899,7 @@ input UpdateDocumentVersionInput {
documentVersionId: ID!
content: String
classification: DocumentClassification
documentType: DocumentType
}
input CancelSignatureRequestInput {

View File

@@ -80,7 +80,6 @@ func NewDocument(document *coredata.Document) *Document {
Organization: &Organization{
ID: document.OrganizationID,
},
DocumentType: document.DocumentType,
CurrentPublishedMajor: document.CurrentPublishedMajor,
CurrentPublishedMinor: document.CurrentPublishedMinor,
TrustCenterVisibility: document.TrustCenterVisibility,

View File

@@ -82,6 +82,7 @@ func NewDocumentVersion(documentVersion *coredata.DocumentVersion) *DocumentVers
Content: documentVersion.Content,
Status: documentVersion.Status,
Classification: documentVersion.Classification,
DocumentType: documentVersion.DocumentType,
PublishedAt: documentVersion.PublishedAt,
Changelog: documentVersion.Changelog,
CreatedAt: documentVersion.CreatedAt,

View File

@@ -70,6 +70,7 @@ type (
Minor int
Status coredata.DocumentVersionStatus
Classification coredata.DocumentClassification
DocumentType coredata.DocumentType
PublishedAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time

View File

@@ -2168,6 +2168,7 @@ func (r *employeeDocumentResolver) Versions(ctx context.Context, obj *types.Empl
Minor: v.Minor,
Status: v.Status,
Classification: v.Classification,
DocumentType: v.DocumentType,
PublishedAt: v.PublishedAt,
CreatedAt: v.CreatedAt,
UpdatedAt: v.UpdatedAt,
@@ -5142,10 +5143,10 @@ func (r *mutationResolver) CreateDocument(ctx context.Context, input types.Creat
ctx,
probo.CreateDocumentRequest{
OrganizationID: input.OrganizationID,
DocumentType: input.DocumentType,
Title: input.Title,
Content: content,
Classification: input.Classification,
DocumentType: input.DocumentType,
TrustCenterVisibility: input.TrustCenterVisibility,
},
)
@@ -5180,7 +5181,6 @@ func (r *mutationResolver) UpdateDocument(ctx context.Context, input types.Updat
probo.UpdateDocumentRequest{
DocumentID: input.ID,
Title: input.Title,
DocumentType: input.DocumentType,
TrustCenterVisibility: input.TrustCenterVisibility,
},
)
@@ -5923,6 +5923,7 @@ func (r *mutationResolver) UpdateDocumentVersion(ctx context.Context, input type
ID: input.DocumentVersionID,
Content: input.Content,
Classification: input.Classification,
DocumentType: input.DocumentType,
},
)
if err != nil {

View File

@@ -2095,7 +2095,6 @@ func (r *Resolver) UpdateDocumentTool(ctx context.Context, req *mcp.CallToolRequ
probo.UpdateDocumentRequest{
DocumentID: input.ID,
Title: input.Title,
DocumentType: input.DocumentType,
TrustCenterVisibility: input.TrustCenterVisibility,
},
)
@@ -2201,6 +2200,7 @@ func (r *Resolver) UpdateDocumentVersionTool(ctx context.Context, req *mcp.CallT
ID: input.DocumentVersionID,
Content: content,
Classification: input.Classification,
DocumentType: input.DocumentType,
},
)
if err != nil {

View File

@@ -5207,7 +5207,6 @@ components:
- id
- organization_id
- title
- document_type
- trust_center_visibility
- status
- created_at
@@ -5222,9 +5221,6 @@ components:
title:
type: string
description: Document title
document_type:
$ref: "#/components/schemas/DocumentType"
description: Document type
current_published_major:
type:
- integer
@@ -5266,6 +5262,7 @@ components:
- major
- minor
- classification
- document_type
- content
- changelog
- status
@@ -5293,6 +5290,9 @@ components:
classification:
$ref: "#/components/schemas/DocumentClassification"
description: Document classification
document_type:
$ref: "#/components/schemas/DocumentType"
description: Document type
content:
type: string
description: Document content
@@ -5482,9 +5482,6 @@ components:
title:
type: string
description: Document title
document_type:
$ref: "#/components/schemas/DocumentType"
description: Document type
trust_center_visibility:
$ref: "#/components/schemas/TrustCenterVisibility"
description: Trust center visibility
@@ -5613,6 +5610,9 @@ components:
classification:
$ref: "#/components/schemas/DocumentClassification"
description: Document classification
document_type:
$ref: "#/components/schemas/DocumentType"
description: Document type
UpdateDocumentVersionOutput:
type: object

View File

@@ -24,7 +24,6 @@ func NewDocument(d *coredata.Document) *Document {
ID: d.ID,
OrganizationID: d.OrganizationID,
Title: d.Title,
DocumentType: d.DocumentType,
CurrentPublishedMajor: d.CurrentPublishedMajor,
CurrentPublishedMinor: d.CurrentPublishedMinor,
TrustCenterVisibility: d.TrustCenterVisibility,
@@ -87,6 +86,7 @@ func NewDocumentVersion(dv *coredata.DocumentVersion) *DocumentVersion {
Major: dv.Major,
Minor: dv.Minor,
Classification: dv.Classification,
DocumentType: dv.DocumentType,
Content: dv.Content,
Changelog: dv.Changelog,
Status: dv.Status,