Move document title ownership from document to version
Title is now owned by document_versions, following the same pattern as classification and document_type. The documents.title column is made nullable with a TODO to drop it. Backend loads title from a latest_versions CTE for ordering purposes only. The frontend resolves title from the latest version, and DocumentTitleForm now operates on DocumentVersion using UpdateDocumentVersion mutation. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -43,11 +43,11 @@ import { LinkedDocumentDialog } from "./LinkedDocumentsDialog";
|
|||||||
const linkedDocumentFragment = graphql`
|
const linkedDocumentFragment = graphql`
|
||||||
fragment LinkedDocumentsCardFragment on Document {
|
fragment LinkedDocumentsCardFragment on Document {
|
||||||
id
|
id
|
||||||
title
|
|
||||||
versions(first: 1) {
|
versions(first: 1) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
|
title
|
||||||
documentType
|
documentType
|
||||||
status
|
status
|
||||||
}
|
}
|
||||||
@@ -217,7 +217,7 @@ function DocumentRow(props: {
|
|||||||
height={36}
|
height={36}
|
||||||
className="border-4 border-highlight rounded box-content"
|
className="border-4 border-highlight rounded box-content"
|
||||||
/>
|
/>
|
||||||
{document.title}
|
{document.versions.edges[0].node.title}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
|
|||||||
@@ -71,10 +71,10 @@ const documentsFragment = graphql`
|
|||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
title
|
|
||||||
versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
|
title
|
||||||
documentType
|
documentType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,7 +135,7 @@ function LinkedDocumentsDialogContent(props: Omit<Props, "children">) {
|
|||||||
|
|
||||||
const filteredDocuments = useMemo(() => {
|
const filteredDocuments = useMemo(() => {
|
||||||
return documents.filter(document =>
|
return documents.filter(document =>
|
||||||
document.title.toLowerCase().includes(search.toLowerCase()),
|
document.versions.edges[0].node.title.toLowerCase().includes(search.toLowerCase()),
|
||||||
);
|
);
|
||||||
}, [documents, search]);
|
}, [documents, search]);
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ function DocumentRow(props: RowProps) {
|
|||||||
className="py-4 flex items-center gap-4 hover:bg-subtle cursor-pointer px-6 w-full h-[100px]"
|
className="py-4 flex items-center gap-4 hover:bg-subtle cursor-pointer px-6 w-full h-[100px]"
|
||||||
onClick={() => onClick(props.document.id)}
|
onClick={() => onClick(props.document.id)}
|
||||||
>
|
>
|
||||||
{props.document.title}
|
{props.document.versions.edges[0].node.title}
|
||||||
<DocumentTypeBadge type={props.document.versions.edges[0].node.documentType} />
|
<DocumentTypeBadge type={props.document.versions.edges[0].node.documentType} />
|
||||||
<Button
|
<Button
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ const documentAccessFragment = graphql`
|
|||||||
status
|
status
|
||||||
document {
|
document {
|
||||||
id
|
id
|
||||||
title
|
|
||||||
versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
|
title
|
||||||
documentType
|
documentType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ function toDocumentAccessInfo(
|
|||||||
return {
|
return {
|
||||||
persisted: node.id !== node.document.id,
|
persisted: node.id !== node.document.id,
|
||||||
variant: "info",
|
variant: "info",
|
||||||
name: node.document.title,
|
name: node.document.versions?.edges[0]?.node.title ?? "",
|
||||||
type: "document",
|
type: "document",
|
||||||
typeLabel: __("Document"),
|
typeLabel: __("Document"),
|
||||||
category: node.document.versions?.edges[0]?.node.documentType ?? "",
|
category: node.document.versions?.edges[0]?.node.documentType ?? "",
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ export const documentLayoutQuery = graphql`
|
|||||||
__typename
|
__typename
|
||||||
... on DocumentVersion {
|
... on DocumentVersion {
|
||||||
id
|
id
|
||||||
|
title
|
||||||
status
|
status
|
||||||
|
...DocumentTitleFormFragment
|
||||||
...DocumentActionsDropdown_versionFragment
|
...DocumentActionsDropdown_versionFragment
|
||||||
...DocumentLayoutDrawer_versionFragment
|
...DocumentLayoutDrawer_versionFragment
|
||||||
signatures(first: 0 filter: { activeContract: true }) {
|
signatures(first: 0 filter: { activeContract: true }) {
|
||||||
@@ -64,14 +66,12 @@ export const documentLayoutQuery = graphql`
|
|||||||
__typename
|
__typename
|
||||||
... on Document {
|
... on Document {
|
||||||
id
|
id
|
||||||
title
|
|
||||||
status
|
status
|
||||||
canPublish: permission(action: "core:document-version:publish")
|
canPublish: permission(action: "core:document-version:publish")
|
||||||
...PublishDialog_documentFragment
|
...PublishDialog_documentFragment
|
||||||
controlInfo: controls(first: 0) {
|
controlInfo: controls(first: 0) {
|
||||||
totalCount
|
totalCount
|
||||||
}
|
}
|
||||||
...DocumentTitleFormFragment
|
|
||||||
...DocumentActionsDropdown_documentFragment
|
...DocumentActionsDropdown_documentFragment
|
||||||
...DocumentLayoutDrawer_documentFragment
|
...DocumentLayoutDrawer_documentFragment
|
||||||
# We use this on /documents/:documentId
|
# We use this on /documents/:documentId
|
||||||
@@ -79,7 +79,9 @@ export const documentLayoutQuery = graphql`
|
|||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
|
title
|
||||||
status
|
status
|
||||||
|
...DocumentTitleFormFragment
|
||||||
...DocumentActionsDropdown_versionFragment
|
...DocumentActionsDropdown_versionFragment
|
||||||
...DocumentLayoutDrawer_versionFragment
|
...DocumentLayoutDrawer_versionFragment
|
||||||
signatures(first: 0 filter: { activeContract: true }) {
|
signatures(first: 0 filter: { activeContract: true }) {
|
||||||
@@ -158,7 +160,7 @@ export function DocumentLayout(props: { queryRef: PreloadedQuery<DocumentLayoutQ
|
|||||||
to: `/organizations/${organizationId}/documents`,
|
to: `/organizations/${organizationId}/documents`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: document.title,
|
label: currentVersion.title,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -182,7 +184,7 @@ export function DocumentLayout(props: { queryRef: PreloadedQuery<DocumentLayoutQ
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={<DocumentTitleForm fKey={document} />}
|
title={<DocumentTitleForm fKey={currentVersion} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import { CurrentUser } from "#/providers/CurrentUser";
|
|||||||
const documentFragment = graphql`
|
const documentFragment = graphql`
|
||||||
fragment DocumentActionsDropdown_documentFragment on Document {
|
fragment DocumentActionsDropdown_documentFragment on Document {
|
||||||
id
|
id
|
||||||
title
|
|
||||||
status
|
status
|
||||||
canUpdate: permission(action: "core:document:update")
|
canUpdate: permission(action: "core:document:update")
|
||||||
canArchive: permission(action: "core:document:archive")
|
canArchive: permission(action: "core:document:archive")
|
||||||
@@ -45,6 +44,7 @@ const documentFragment = graphql`
|
|||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
|
title
|
||||||
status
|
status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,6 +120,7 @@ const unarchiveDocumentMutation = graphql`
|
|||||||
const versionFragment = graphql`
|
const versionFragment = graphql`
|
||||||
fragment DocumentActionsDropdown_versionFragment on DocumentVersion {
|
fragment DocumentActionsDropdown_versionFragment on DocumentVersion {
|
||||||
id
|
id
|
||||||
|
title
|
||||||
major
|
major
|
||||||
minor
|
minor
|
||||||
status
|
status
|
||||||
@@ -226,7 +227,7 @@ export function DocumentActionsDropdown(props: {
|
|||||||
{
|
{
|
||||||
message: sprintf(
|
message: sprintf(
|
||||||
__("This will archive the document \"%s\". It will no longer be editable."),
|
__("This will archive the document \"%s\". It will no longer be editable."),
|
||||||
document.title,
|
lastVersion.title,
|
||||||
),
|
),
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
label: __("Archive"),
|
label: __("Archive"),
|
||||||
@@ -272,7 +273,7 @@ export function DocumentActionsDropdown(props: {
|
|||||||
__(
|
__(
|
||||||
"This will permanently delete the document \"%s\". This action cannot be undone.",
|
"This will permanently delete the document \"%s\". This action cannot be undone.",
|
||||||
),
|
),
|
||||||
document.title,
|
lastVersion.title,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -306,7 +307,7 @@ export function DocumentActionsDropdown(props: {
|
|||||||
"This will permanently delete the draft version %s of \"%s\". This action cannot be undone.",
|
"This will permanently delete the draft version %s of \"%s\". This action cannot be undone.",
|
||||||
),
|
),
|
||||||
`${version.major}.${version.minor}`,
|
`${version.major}.${version.minor}`,
|
||||||
document.title,
|
lastVersion.title,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -340,7 +341,7 @@ export function DocumentActionsDropdown(props: {
|
|||||||
if (data.exportDocumentVersionPDF) {
|
if (data.exportDocumentVersionPDF) {
|
||||||
const link = window.document.createElement("a");
|
const link = window.document.createElement("a");
|
||||||
link.href = data.exportDocumentVersionPDF.data;
|
link.href = data.exportDocumentVersionPDF.data;
|
||||||
link.download = `${document.title}-v${version.major}.${version.minor}.pdf`;
|
link.download = `${version.title}-v${version.major}.${version.minor}.pdf`;
|
||||||
window.document.body.appendChild(link);
|
window.document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
window.document.body.removeChild(link);
|
window.document.body.removeChild(link);
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import { useOrganizationId } from "#/hooks/useOrganizationId";
|
|||||||
const fragment = graphql`
|
const fragment = graphql`
|
||||||
fragment DocumentListItemFragment on Document {
|
fragment DocumentListItemFragment on Document {
|
||||||
id
|
id
|
||||||
title
|
|
||||||
updatedAt
|
updatedAt
|
||||||
canDelete: permission(action: "core:document:delete")
|
canDelete: permission(action: "core:document:delete")
|
||||||
defaultApprovers {
|
defaultApprovers {
|
||||||
@@ -36,6 +35,7 @@ const fragment = graphql`
|
|||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
|
title
|
||||||
status
|
status
|
||||||
major
|
major
|
||||||
minor
|
minor
|
||||||
@@ -135,7 +135,7 @@ export function DocumentListItem(props: {
|
|||||||
__(
|
__(
|
||||||
"This will permanently delete the document \"%s\". This action cannot be undone.",
|
"This will permanently delete the document \"%s\". This action cannot be undone.",
|
||||||
),
|
),
|
||||||
document.title,
|
lastVersion.title,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -149,7 +149,7 @@ export function DocumentListItem(props: {
|
|||||||
<Checkbox checked={checked} onChange={onCheck} />
|
<Checkbox checked={checked} onChange={onCheck} />
|
||||||
</Td>
|
</Td>
|
||||||
<Td className="min-w-0">
|
<Td className="min-w-0">
|
||||||
<div className="flex gap-4 items-center">{document.title}</div>
|
<div className="flex gap-4 items-center">{lastVersion.title}</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td className="w-24">
|
<Td className="w-24">
|
||||||
<Badge variant={statusVariant[lastVersion.status]}>
|
<Badge variant={statusVariant[lastVersion.status]}>
|
||||||
|
|||||||
@@ -12,22 +12,22 @@
|
|||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { formatError } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { Button, IconCheckmark1, IconCrossLargeX, IconPencil, Input } from "@probo/ui";
|
import { Button, IconCheckmark1, IconCrossLargeX, IconPencil, Input, useToast } from "@probo/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useFragment } from "react-relay";
|
import { useFragment, useMutation } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { DocumentTitleFormFragment$key } from "#/__generated__/core/DocumentTitleFormFragment.graphql";
|
import type { DocumentTitleFormFragment$key } from "#/__generated__/core/DocumentTitleFormFragment.graphql";
|
||||||
import type { DocumentTitleFormMutation } from "#/__generated__/core/DocumentTitleFormMutation.graphql";
|
import type { DocumentTitleFormMutation } from "#/__generated__/core/DocumentTitleFormMutation.graphql";
|
||||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||||
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
|
||||||
|
|
||||||
const updateDocumentTitleMutation = graphql`
|
const updateDocumentVersionTitleMutation = graphql`
|
||||||
mutation DocumentTitleFormMutation($input: UpdateDocumentInput!) {
|
mutation DocumentTitleFormMutation($input: UpdateDocumentVersionInput!) {
|
||||||
updateDocument(input: $input) {
|
updateDocumentVersion(input: $input) {
|
||||||
document {
|
documentVersion {
|
||||||
...DocumentTitleFormFragment
|
...DocumentTitleFormFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,10 +35,11 @@ const updateDocumentTitleMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const fragment = graphql`
|
const fragment = graphql`
|
||||||
fragment DocumentTitleFormFragment on Document {
|
fragment DocumentTitleFormFragment on DocumentVersion {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
canUpdate: permission(action: "core:document:update")
|
status
|
||||||
|
canUpdate: permission(action: "core:document-version:update")
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -50,38 +51,40 @@ export function DocumentTitleForm(props: { fKey: DocumentTitleFormFragment$key }
|
|||||||
const { fKey } = props;
|
const { fKey } = props;
|
||||||
|
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const document = useFragment<DocumentTitleFormFragment$key>(fragment, fKey);
|
const version = useFragment<DocumentTitleFormFragment$key>(fragment, fKey);
|
||||||
const [updateDocument, isUpdatingDocument]
|
const [updateDocumentVersion, isUpdating]
|
||||||
= useMutationWithToasts<DocumentTitleFormMutation>(
|
= useMutation<DocumentTitleFormMutation>(updateDocumentVersionTitleMutation);
|
||||||
updateDocumentTitleMutation,
|
|
||||||
{
|
|
||||||
successMessage: __("Document updated successfully."),
|
|
||||||
errorMessage: __("Failed to update document"),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||||
const { register, handleSubmit, reset } = useFormWithSchema(
|
const { register, handleSubmit, reset } = useFormWithSchema(
|
||||||
schema,
|
schema,
|
||||||
{
|
{
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: document.title,
|
title: version.title,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleUpdateTitle = async (data: { title: string }) => {
|
const handleUpdateTitle = (data: { title: string }) => {
|
||||||
await updateDocument({
|
updateDocumentVersion({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
id: document.id,
|
documentVersionId: version.id,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onCompleted(_, errors) {
|
||||||
|
if (errors?.length) {
|
||||||
|
toast({ title: __("Error"), description: formatError(__("Failed to update document"), errors), variant: "error" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsEditingTitle(false);
|
setIsEditingTitle(false);
|
||||||
},
|
},
|
||||||
|
onError(error) {
|
||||||
|
toast({ title: __("Error"), description: error.message, variant: "error" });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,7 +110,7 @@ export function DocumentTitleForm(props: { fKey: DocumentTitleFormFragment$key }
|
|||||||
variant="quaternary"
|
variant="quaternary"
|
||||||
icon={IconCheckmark1}
|
icon={IconCheckmark1}
|
||||||
onClick={() => void handleSubmit(handleUpdateTitle)()}
|
onClick={() => void handleSubmit(handleUpdateTitle)()}
|
||||||
disabled={isUpdatingDocument}
|
disabled={isUpdating}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="quaternary"
|
variant="quaternary"
|
||||||
@@ -121,8 +124,8 @@ export function DocumentTitleForm(props: { fKey: DocumentTitleFormFragment$key }
|
|||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{document.title}</span>
|
<span>{version.title}</span>
|
||||||
{document.canUpdate && (
|
{version.canUpdate && version.status === "DRAFT" && (
|
||||||
<Button
|
<Button
|
||||||
variant="quaternary"
|
variant="quaternary"
|
||||||
icon={IconPencil}
|
icon={IconPencil}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func TestDocument_Create(t *testing.T) {
|
|||||||
"documentType": "POLICY",
|
"documentType": "POLICY",
|
||||||
"classification": "INTERNAL",
|
"classification": "INTERNAL",
|
||||||
},
|
},
|
||||||
assertField: "title",
|
assertField: "versionTitle",
|
||||||
assertValue: "Security Policy",
|
assertValue: "Security Policy",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -102,12 +102,12 @@ func TestDocument_Create(t *testing.T) {
|
|||||||
documentEdge {
|
documentEdge {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
title
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
documentVersionEdge {
|
documentVersionEdge {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
|
title
|
||||||
documentType
|
documentType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,13 +124,13 @@ func TestDocument_Create(t *testing.T) {
|
|||||||
CreateDocument struct {
|
CreateDocument struct {
|
||||||
DocumentEdge struct {
|
DocumentEdge struct {
|
||||||
Node struct {
|
Node struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
|
||||||
} `json:"node"`
|
} `json:"node"`
|
||||||
} `json:"documentEdge"`
|
} `json:"documentEdge"`
|
||||||
DocumentVersionEdge struct {
|
DocumentVersionEdge struct {
|
||||||
Node struct {
|
Node struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
DocumentType string `json:"documentType"`
|
DocumentType string `json:"documentType"`
|
||||||
} `json:"node"`
|
} `json:"node"`
|
||||||
} `json:"documentVersionEdge"`
|
} `json:"documentVersionEdge"`
|
||||||
@@ -146,8 +146,8 @@ func TestDocument_Create(t *testing.T) {
|
|||||||
versionNode := result.CreateDocument.DocumentVersionEdge.Node
|
versionNode := result.CreateDocument.DocumentVersionEdge.Node
|
||||||
|
|
||||||
switch tt.assertField {
|
switch tt.assertField {
|
||||||
case "title":
|
case "versionTitle":
|
||||||
assert.Equal(t, tt.assertValue, node.Title)
|
assert.Equal(t, tt.assertValue, versionNode.Title)
|
||||||
case "documentType":
|
case "documentType":
|
||||||
assert.Equal(t, tt.assertValue, versionNode.DocumentType)
|
assert.Equal(t, tt.assertValue, versionNode.DocumentType)
|
||||||
}
|
}
|
||||||
@@ -290,18 +290,19 @@ func TestDocument_Update(t *testing.T) {
|
|||||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
|
||||||
t.Run(
|
t.Run(
|
||||||
"update title",
|
"update title via document version",
|
||||||
func(t *testing.T) {
|
func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
documentID := factory.NewDocument(owner).
|
doc := factory.NewDocument(owner).
|
||||||
WithTitle("Document to Update").
|
WithTitle("Document to Update")
|
||||||
Create()
|
doc.Create()
|
||||||
|
versionID := doc.VersionID()
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
mutation UpdateDocument($input: UpdateDocumentInput!) {
|
mutation UpdateDocumentVersion($input: UpdateDocumentVersionInput!) {
|
||||||
updateDocument(input: $input) {
|
updateDocumentVersion(input: $input) {
|
||||||
document {
|
documentVersion {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
}
|
}
|
||||||
@@ -310,31 +311,33 @@ func TestDocument_Update(t *testing.T) {
|
|||||||
`
|
`
|
||||||
|
|
||||||
var result struct {
|
var result struct {
|
||||||
UpdateDocument struct {
|
UpdateDocumentVersion struct {
|
||||||
Document struct {
|
DocumentVersion struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
} `json:"document"`
|
} `json:"documentVersion"`
|
||||||
} `json:"updateDocument"`
|
} `json:"updateDocumentVersion"`
|
||||||
}
|
}
|
||||||
|
|
||||||
err := owner.Execute(query, map[string]any{
|
err := owner.Execute(query, map[string]any{
|
||||||
"input": map[string]any{
|
"input": map[string]any{
|
||||||
"id": documentID,
|
"documentVersionId": versionID,
|
||||||
"title": "Updated Document Title",
|
"title": "Updated Document Title",
|
||||||
},
|
},
|
||||||
}, &result)
|
}, &result)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "Updated Document Title", result.UpdateDocument.Document.Title)
|
assert.Equal(t, "Updated Document Title", result.UpdateDocumentVersion.DocumentVersion.Title)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDocument_Update_Validation(t *testing.T) {
|
func TestDocumentVersion_Update_TitleValidation(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
|
||||||
baseDocumentID := factory.NewDocument(owner).WithTitle("Validation Test Document").Create()
|
doc := factory.NewDocument(owner).WithTitle("Validation Test Document")
|
||||||
|
doc.Create()
|
||||||
|
baseVersionID := doc.VersionID()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -342,51 +345,43 @@ func TestDocument_Update_Validation(t *testing.T) {
|
|||||||
input func(id string) map[string]any
|
input func(id string) map[string]any
|
||||||
wantErrorContains string
|
wantErrorContains string
|
||||||
}{
|
}{
|
||||||
{
|
|
||||||
name: "invalid ID format",
|
|
||||||
setup: func() string { return "invalid-id-format" },
|
|
||||||
input: func(id string) map[string]any {
|
|
||||||
return map[string]any{"id": id, "title": "Test"}
|
|
||||||
},
|
|
||||||
wantErrorContains: "base64",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "title with HTML tags",
|
name: "title with HTML tags",
|
||||||
setup: func() string { return baseDocumentID },
|
setup: func() string { return baseVersionID },
|
||||||
input: func(id string) map[string]any {
|
input: func(id string) map[string]any {
|
||||||
return map[string]any{"id": id, "title": "<script>alert('xss')</script>"}
|
return map[string]any{"documentVersionId": id, "title": "<script>alert('xss')</script>"}
|
||||||
},
|
},
|
||||||
wantErrorContains: "HTML",
|
wantErrorContains: "HTML",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "title with newline",
|
name: "title with newline",
|
||||||
setup: func() string { return baseDocumentID },
|
setup: func() string { return baseVersionID },
|
||||||
input: func(id string) map[string]any {
|
input: func(id string) map[string]any {
|
||||||
return map[string]any{"id": id, "title": "Test\nDocument"}
|
return map[string]any{"documentVersionId": id, "title": "Test\nDocument"}
|
||||||
},
|
},
|
||||||
wantErrorContains: "newline",
|
wantErrorContains: "newline",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "title with carriage return",
|
name: "title with carriage return",
|
||||||
setup: func() string { return baseDocumentID },
|
setup: func() string { return baseVersionID },
|
||||||
input: func(id string) map[string]any {
|
input: func(id string) map[string]any {
|
||||||
return map[string]any{"id": id, "title": "Test\rDocument"}
|
return map[string]any{"documentVersionId": id, "title": "Test\rDocument"}
|
||||||
},
|
},
|
||||||
wantErrorContains: "carriage return",
|
wantErrorContains: "carriage return",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "title with null byte",
|
name: "title with null byte",
|
||||||
setup: func() string { return baseDocumentID },
|
setup: func() string { return baseVersionID },
|
||||||
input: func(id string) map[string]any {
|
input: func(id string) map[string]any {
|
||||||
return map[string]any{"id": id, "title": "Test\x00Document"}
|
return map[string]any{"documentVersionId": id, "title": "Test\x00Document"}
|
||||||
},
|
},
|
||||||
wantErrorContains: "control character",
|
wantErrorContains: "control character",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "title with zero-width space",
|
name: "title with zero-width space",
|
||||||
setup: func() string { return baseDocumentID },
|
setup: func() string { return baseVersionID },
|
||||||
input: func(id string) map[string]any {
|
input: func(id string) map[string]any {
|
||||||
return map[string]any{"id": id, "title": "Test\u200BDocument"}
|
return map[string]any{"documentVersionId": id, "title": "Test\u200BDocument"}
|
||||||
},
|
},
|
||||||
wantErrorContains: "zero-width",
|
wantErrorContains: "zero-width",
|
||||||
},
|
},
|
||||||
@@ -394,19 +389,19 @@ func TestDocument_Update_Validation(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
documentID := tt.setup()
|
versionID := tt.setup()
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
mutation UpdateDocument($input: UpdateDocumentInput!) {
|
mutation UpdateDocumentVersion($input: UpdateDocumentVersionInput!) {
|
||||||
updateDocument(input: $input) {
|
updateDocumentVersion(input: $input) {
|
||||||
document {
|
documentVersion {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
_, err := owner.Do(query, map[string]any{"input": tt.input(documentID)})
|
_, err := owner.Do(query, map[string]any{"input": tt.input(versionID)})
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), tt.wantErrorContains)
|
assert.Contains(t, err.Error(), tt.wantErrorContains)
|
||||||
})
|
})
|
||||||
@@ -494,7 +489,6 @@ func TestDocument_List(t *testing.T) {
|
|||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
title
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
totalCount
|
totalCount
|
||||||
@@ -509,8 +503,7 @@ func TestDocument_List(t *testing.T) {
|
|||||||
Documents struct {
|
Documents struct {
|
||||||
Edges []struct {
|
Edges []struct {
|
||||||
Node struct {
|
Node struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
|
||||||
} `json:"node"`
|
} `json:"node"`
|
||||||
} `json:"edges"`
|
} `json:"edges"`
|
||||||
TotalCount int `json:"totalCount"`
|
TotalCount int `json:"totalCount"`
|
||||||
@@ -535,7 +528,6 @@ func TestDocument_Query(t *testing.T) {
|
|||||||
node(id: $id) {
|
node(id: $id) {
|
||||||
... on Document {
|
... on Document {
|
||||||
id
|
id
|
||||||
title
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -648,8 +640,8 @@ func TestDocument_Timestamps(t *testing.T) {
|
|||||||
|
|
||||||
err = owner.Execute(updateQuery, map[string]any{
|
err = owner.Execute(updateQuery, map[string]any{
|
||||||
"input": map[string]any{
|
"input": map[string]any{
|
||||||
"id": documentID,
|
"id": documentID,
|
||||||
"title": "Updated Timestamp Test",
|
"trustCenterVisibility": "PRIVATE",
|
||||||
},
|
},
|
||||||
}, &updateResult)
|
}, &updateResult)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -781,8 +773,8 @@ func TestDocument_RBAC(t *testing.T) {
|
|||||||
}
|
}
|
||||||
`, map[string]any{
|
`, map[string]any{
|
||||||
"input": map[string]any{
|
"input": map[string]any{
|
||||||
"id": documentID,
|
"id": documentID,
|
||||||
"title": "Updated by Owner",
|
"trustCenterVisibility": "PRIVATE",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
require.NoError(t, err, "owner should be able to update document")
|
require.NoError(t, err, "owner should be able to update document")
|
||||||
@@ -802,8 +794,8 @@ func TestDocument_RBAC(t *testing.T) {
|
|||||||
}
|
}
|
||||||
`, map[string]any{
|
`, map[string]any{
|
||||||
"input": map[string]any{
|
"input": map[string]any{
|
||||||
"id": documentID,
|
"id": documentID,
|
||||||
"title": "Updated by Admin",
|
"trustCenterVisibility": "PRIVATE",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
require.NoError(t, err, "admin should be able to update document")
|
require.NoError(t, err, "admin should be able to update document")
|
||||||
@@ -823,8 +815,8 @@ func TestDocument_RBAC(t *testing.T) {
|
|||||||
}
|
}
|
||||||
`, map[string]any{
|
`, map[string]any{
|
||||||
"input": map[string]any{
|
"input": map[string]any{
|
||||||
"id": documentID,
|
"id": documentID,
|
||||||
"title": "Updated by Viewer",
|
"trustCenterVisibility": "PRIVATE",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
testutil.RequireForbiddenError(t, err, "viewer should not be able to update document")
|
testutil.RequireForbiddenError(t, err, "viewer should not be able to update document")
|
||||||
@@ -894,15 +886,14 @@ func TestDocument_RBAC(t *testing.T) {
|
|||||||
|
|
||||||
var result struct {
|
var result struct {
|
||||||
Node *struct {
|
Node *struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
|
||||||
} `json:"node"`
|
} `json:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
err := owner.Execute(`
|
err := owner.Execute(`
|
||||||
query($id: ID!) {
|
query($id: ID!) {
|
||||||
node(id: $id) {
|
node(id: $id) {
|
||||||
... on Document { id title }
|
... on Document { id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, map[string]any{"id": documentID}, &result)
|
`, map[string]any{"id": documentID}, &result)
|
||||||
@@ -918,15 +909,14 @@ func TestDocument_RBAC(t *testing.T) {
|
|||||||
|
|
||||||
var result struct {
|
var result struct {
|
||||||
Node *struct {
|
Node *struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
|
||||||
} `json:"node"`
|
} `json:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
err := admin.Execute(`
|
err := admin.Execute(`
|
||||||
query($id: ID!) {
|
query($id: ID!) {
|
||||||
node(id: $id) {
|
node(id: $id) {
|
||||||
... on Document { id title }
|
... on Document { id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, map[string]any{"id": documentID}, &result)
|
`, map[string]any{"id": documentID}, &result)
|
||||||
@@ -942,15 +932,14 @@ func TestDocument_RBAC(t *testing.T) {
|
|||||||
|
|
||||||
var result struct {
|
var result struct {
|
||||||
Node *struct {
|
Node *struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
|
||||||
} `json:"node"`
|
} `json:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
err := viewer.Execute(`
|
err := viewer.Execute(`
|
||||||
query($id: ID!) {
|
query($id: ID!) {
|
||||||
node(id: $id) {
|
node(id: $id) {
|
||||||
... on Document { id title }
|
... on Document { id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, map[string]any{"id": documentID}, &result)
|
`, map[string]any{"id": documentID}, &result)
|
||||||
@@ -990,21 +979,23 @@ func TestDocument_MaxLength_Validation(t *testing.T) {
|
|||||||
assert.Contains(t, err.Error(), "title")
|
assert.Contains(t, err.Error(), "title")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("update with long title", func(t *testing.T) {
|
t.Run("update version", func(t *testing.T) {
|
||||||
documentID := factory.NewDocument(owner).WithTitle("Max Length Test").Create()
|
doc := factory.NewDocument(owner).WithTitle("Max Length Test")
|
||||||
|
doc.Create()
|
||||||
|
versionID := doc.VersionID()
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
mutation UpdateDocument($input: UpdateDocumentInput!) {
|
mutation UpdateDocumentVersion($input: UpdateDocumentVersionInput!) {
|
||||||
updateDocument(input: $input) {
|
updateDocumentVersion(input: $input) {
|
||||||
document { id }
|
documentVersion { id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
_, err := owner.Do(query, map[string]any{
|
_, err := owner.Do(query, map[string]any{
|
||||||
"input": map[string]any{
|
"input": map[string]any{
|
||||||
"id": documentID,
|
"documentVersionId": versionID,
|
||||||
"title": longTitle,
|
"title": longTitle,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -1080,7 +1071,7 @@ func TestDocument_Pagination(t *testing.T) {
|
|||||||
... on Organization {
|
... on Organization {
|
||||||
documents(first: 2) {
|
documents(first: 2) {
|
||||||
edges {
|
edges {
|
||||||
node { id title }
|
node { id }
|
||||||
cursor
|
cursor
|
||||||
}
|
}
|
||||||
pageInfo {
|
pageInfo {
|
||||||
@@ -1101,8 +1092,7 @@ func TestDocument_Pagination(t *testing.T) {
|
|||||||
Documents struct {
|
Documents struct {
|
||||||
Edges []struct {
|
Edges []struct {
|
||||||
Node struct {
|
Node struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
|
||||||
} `json:"node"`
|
} `json:"node"`
|
||||||
Cursor string `json:"cursor"`
|
Cursor string `json:"cursor"`
|
||||||
} `json:"edges"`
|
} `json:"edges"`
|
||||||
@@ -1131,7 +1121,7 @@ func TestDocument_Pagination(t *testing.T) {
|
|||||||
... on Organization {
|
... on Organization {
|
||||||
documents(first: 2, after: $after) {
|
documents(first: 2, after: $after) {
|
||||||
edges {
|
edges {
|
||||||
node { id title }
|
node { id }
|
||||||
}
|
}
|
||||||
pageInfo {
|
pageInfo {
|
||||||
hasNextPage
|
hasNextPage
|
||||||
@@ -1148,8 +1138,7 @@ func TestDocument_Pagination(t *testing.T) {
|
|||||||
Documents struct {
|
Documents struct {
|
||||||
Edges []struct {
|
Edges []struct {
|
||||||
Node struct {
|
Node struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
|
||||||
} `json:"node"`
|
} `json:"node"`
|
||||||
} `json:"edges"`
|
} `json:"edges"`
|
||||||
PageInfo testutil.PageInfo `json:"pageInfo"`
|
PageInfo testutil.PageInfo `json:"pageInfo"`
|
||||||
@@ -1173,7 +1162,7 @@ func TestDocument_Pagination(t *testing.T) {
|
|||||||
... on Organization {
|
... on Organization {
|
||||||
documents(last: 2) {
|
documents(last: 2) {
|
||||||
edges {
|
edges {
|
||||||
node { id title }
|
node { id }
|
||||||
}
|
}
|
||||||
pageInfo {
|
pageInfo {
|
||||||
hasNextPage
|
hasNextPage
|
||||||
@@ -1190,8 +1179,7 @@ func TestDocument_Pagination(t *testing.T) {
|
|||||||
Documents struct {
|
Documents struct {
|
||||||
Edges []struct {
|
Edges []struct {
|
||||||
Node struct {
|
Node struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
|
||||||
} `json:"node"`
|
} `json:"node"`
|
||||||
} `json:"edges"`
|
} `json:"edges"`
|
||||||
PageInfo testutil.PageInfo `json:"pageInfo"`
|
PageInfo testutil.PageInfo `json:"pageInfo"`
|
||||||
@@ -1222,7 +1210,6 @@ func TestDocument_TenantIsolation(t *testing.T) {
|
|||||||
node(id: $id) {
|
node(id: $id) {
|
||||||
... on Document {
|
... on Document {
|
||||||
id
|
id
|
||||||
title
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1230,8 +1217,7 @@ func TestDocument_TenantIsolation(t *testing.T) {
|
|||||||
|
|
||||||
var result struct {
|
var result struct {
|
||||||
Node *struct {
|
Node *struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
|
||||||
} `json:"node"`
|
} `json:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1250,8 +1236,8 @@ func TestDocument_TenantIsolation(t *testing.T) {
|
|||||||
|
|
||||||
_, err := org2Owner.Do(query, map[string]any{
|
_, err := org2Owner.Do(query, map[string]any{
|
||||||
"input": map[string]any{
|
"input": map[string]any{
|
||||||
"id": documentID,
|
"id": documentID,
|
||||||
"title": "Hijacked Document",
|
"trustCenterVisibility": "PRIVATE",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
require.Error(t, err, "Should not be able to update document from another org")
|
require.Error(t, err, "Should not be able to update document from another org")
|
||||||
@@ -1283,7 +1269,6 @@ func TestDocument_TenantIsolation(t *testing.T) {
|
|||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
title
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1297,8 +1282,7 @@ func TestDocument_TenantIsolation(t *testing.T) {
|
|||||||
Documents struct {
|
Documents struct {
|
||||||
Edges []struct {
|
Edges []struct {
|
||||||
Node struct {
|
Node struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
|
||||||
} `json:"node"`
|
} `json:"node"`
|
||||||
} `json:"edges"`
|
} `json:"edges"`
|
||||||
} `json:"documents"`
|
} `json:"documents"`
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ func TestEmployeeDocument_NodeAccess(t *testing.T) {
|
|||||||
node(id: $id) {
|
node(id: $id) {
|
||||||
... on Document {
|
... on Document {
|
||||||
id
|
id
|
||||||
title
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +70,6 @@ func TestEmployeeDocument_NodeAccess(t *testing.T) {
|
|||||||
node(id: $id) {
|
node(id: $id) {
|
||||||
... on Document {
|
... on Document {
|
||||||
id
|
id
|
||||||
title
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -627,7 +627,6 @@ func TestRisk_SubResolvers(t *testing.T) {
|
|||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
title
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -642,8 +641,7 @@ func TestRisk_SubResolvers(t *testing.T) {
|
|||||||
Documents struct {
|
Documents struct {
|
||||||
Edges []struct {
|
Edges []struct {
|
||||||
Node struct {
|
Node struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
|
||||||
} `json:"node"`
|
} `json:"node"`
|
||||||
} `json:"edges"`
|
} `json:"edges"`
|
||||||
} `json:"documents"`
|
} `json:"documents"`
|
||||||
|
|||||||
@@ -825,50 +825,10 @@ func (b *MeetingBuilder) Create() string {
|
|||||||
return CreateMeeting(b.client, b.attrs)
|
return CreateMeeting(b.client, b.attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateDocument(c *testutil.Client, attrs ...Attrs) string {
|
|
||||||
c.T.Helper()
|
|
||||||
|
|
||||||
var a Attrs
|
|
||||||
if len(attrs) > 0 {
|
|
||||||
a = attrs[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = `
|
|
||||||
mutation($input: CreateDocumentInput!) {
|
|
||||||
createDocument(input: $input) {
|
|
||||||
documentEdge {
|
|
||||||
node { id }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
input := map[string]any{
|
|
||||||
"organizationId": c.GetOrganizationID().String(),
|
|
||||||
"title": a.getString("title", SafeName("Document")),
|
|
||||||
"documentType": a.getString("documentType", "POLICY"),
|
|
||||||
"classification": a.getString("classification", "INTERNAL"),
|
|
||||||
}
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
CreateDocument struct {
|
|
||||||
DocumentEdge struct {
|
|
||||||
Node struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
} `json:"node"`
|
|
||||||
} `json:"documentEdge"`
|
|
||||||
} `json:"createDocument"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := c.Execute(query, map[string]any{"input": input}, &result)
|
|
||||||
require.NoError(c.T, err, "createDocument mutation failed")
|
|
||||||
|
|
||||||
return result.CreateDocument.DocumentEdge.Node.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
type DocumentBuilder struct {
|
type DocumentBuilder struct {
|
||||||
client *testutil.Client
|
client *testutil.Client
|
||||||
attrs Attrs
|
attrs Attrs
|
||||||
|
versionID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDocument(c *testutil.Client) *DocumentBuilder {
|
func NewDocument(c *testutil.Client) *DocumentBuilder {
|
||||||
@@ -895,8 +855,56 @@ func (b *DocumentBuilder) WithClassification(classification string) *DocumentBui
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *DocumentBuilder) VersionID() string {
|
||||||
|
return b.versionID
|
||||||
|
}
|
||||||
|
|
||||||
func (b *DocumentBuilder) Create() string {
|
func (b *DocumentBuilder) Create() string {
|
||||||
return CreateDocument(b.client, b.attrs)
|
b.client.T.Helper()
|
||||||
|
|
||||||
|
a := b.attrs
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
mutation($input: CreateDocumentInput!) {
|
||||||
|
createDocument(input: $input) {
|
||||||
|
documentEdge {
|
||||||
|
node { id }
|
||||||
|
}
|
||||||
|
documentVersionEdge {
|
||||||
|
node { id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
input := map[string]any{
|
||||||
|
"organizationId": b.client.GetOrganizationID().String(),
|
||||||
|
"title": a.getString("title", SafeName("Document")),
|
||||||
|
"documentType": a.getString("documentType", "POLICY"),
|
||||||
|
"classification": a.getString("classification", "INTERNAL"),
|
||||||
|
}
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
CreateDocument struct {
|
||||||
|
DocumentEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentEdge"`
|
||||||
|
DocumentVersionEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentVersionEdge"`
|
||||||
|
} `json:"createDocument"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := b.client.Execute(query, map[string]any{"input": input}, &result)
|
||||||
|
require.NoError(b.client.T, err, "createDocument mutation failed")
|
||||||
|
|
||||||
|
b.versionID = result.CreateDocument.DocumentVersionEdge.Node.ID
|
||||||
|
|
||||||
|
return result.CreateDocument.DocumentEdge.Node.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateProcessingActivity(c *testutil.Client, attrs ...Attrs) string {
|
func CreateProcessingActivity(c *testutil.Client, attrs ...Attrs) string {
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ type (
|
|||||||
Document struct {
|
Document struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
OrganizationID gid.GID `db:"organization_id"`
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
Title string `db:"title"`
|
|
||||||
CurrentPublishedMajor *int `db:"current_published_major"`
|
CurrentPublishedMajor *int `db:"current_published_major"`
|
||||||
CurrentPublishedMinor *int `db:"current_published_minor"`
|
CurrentPublishedMinor *int `db:"current_published_minor"`
|
||||||
TrustCenterVisibility TrustCenterVisibility `db:"trust_center_visibility"`
|
TrustCenterVisibility TrustCenterVisibility `db:"trust_center_visibility"`
|
||||||
@@ -42,6 +41,7 @@ type (
|
|||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
|
|
||||||
// ordering only
|
// ordering only
|
||||||
|
Title string `db:"title"`
|
||||||
DocumentType DocumentType `db:"document_type"`
|
DocumentType DocumentType `db:"document_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,14 +93,13 @@ func (p *Document) LoadByID(
|
|||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
WITH latest_versions AS (
|
WITH latest_versions AS (
|
||||||
SELECT DISTINCT ON (document_id) document_id, document_type
|
SELECT DISTINCT ON (document_id) document_id, title, document_type
|
||||||
FROM document_versions
|
FROM document_versions
|
||||||
ORDER BY document_id, major DESC, minor DESC
|
ORDER BY document_id, major DESC, minor DESC
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
documents.id,
|
documents.id,
|
||||||
documents.organization_id,
|
documents.organization_id,
|
||||||
documents.title,
|
|
||||||
documents.current_published_major,
|
documents.current_published_major,
|
||||||
documents.current_published_minor,
|
documents.current_published_minor,
|
||||||
documents.trust_center_visibility,
|
documents.trust_center_visibility,
|
||||||
@@ -108,6 +107,7 @@ SELECT
|
|||||||
documents.archived_at,
|
documents.archived_at,
|
||||||
documents.created_at,
|
documents.created_at,
|
||||||
documents.updated_at,
|
documents.updated_at,
|
||||||
|
COALESCE(lv.title, '') AS title,
|
||||||
COALESCE(lv.document_type, 'OTHER') AS document_type
|
COALESCE(lv.document_type, 'OTHER') AS document_type
|
||||||
FROM
|
FROM
|
||||||
documents
|
documents
|
||||||
@@ -152,14 +152,13 @@ func (p *Document) LoadByIDWithFilter(
|
|||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
WITH latest_versions AS (
|
WITH latest_versions AS (
|
||||||
SELECT DISTINCT ON (document_id) document_id, document_type
|
SELECT DISTINCT ON (document_id) document_id, title, document_type
|
||||||
FROM document_versions
|
FROM document_versions
|
||||||
ORDER BY document_id, major DESC, minor DESC
|
ORDER BY document_id, major DESC, minor DESC
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
documents.id,
|
documents.id,
|
||||||
documents.organization_id,
|
documents.organization_id,
|
||||||
documents.title,
|
|
||||||
documents.current_published_major,
|
documents.current_published_major,
|
||||||
documents.current_published_minor,
|
documents.current_published_minor,
|
||||||
documents.trust_center_visibility,
|
documents.trust_center_visibility,
|
||||||
@@ -167,6 +166,7 @@ SELECT
|
|||||||
documents.archived_at,
|
documents.archived_at,
|
||||||
documents.created_at,
|
documents.created_at,
|
||||||
documents.updated_at,
|
documents.updated_at,
|
||||||
|
COALESCE(lv.title, '') AS title,
|
||||||
COALESCE(lv.document_type, 'OTHER') AS document_type
|
COALESCE(lv.document_type, 'OTHER') AS document_type
|
||||||
FROM
|
FROM
|
||||||
documents
|
documents
|
||||||
@@ -212,14 +212,13 @@ func (p *Documents) LoadByIDs(
|
|||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
WITH latest_versions AS (
|
WITH latest_versions AS (
|
||||||
SELECT DISTINCT ON (document_id) document_id, document_type
|
SELECT DISTINCT ON (document_id) document_id, title, document_type
|
||||||
FROM document_versions
|
FROM document_versions
|
||||||
ORDER BY document_id, major DESC, minor DESC
|
ORDER BY document_id, major DESC, minor DESC
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
documents.id,
|
documents.id,
|
||||||
documents.organization_id,
|
documents.organization_id,
|
||||||
documents.title,
|
|
||||||
documents.current_published_major,
|
documents.current_published_major,
|
||||||
documents.current_published_minor,
|
documents.current_published_minor,
|
||||||
documents.trust_center_visibility,
|
documents.trust_center_visibility,
|
||||||
@@ -227,6 +226,7 @@ SELECT
|
|||||||
documents.archived_at,
|
documents.archived_at,
|
||||||
documents.created_at,
|
documents.created_at,
|
||||||
documents.updated_at,
|
documents.updated_at,
|
||||||
|
COALESCE(lv.title, '') AS title,
|
||||||
COALESCE(lv.document_type, 'OTHER') AS document_type
|
COALESCE(lv.document_type, 'OTHER') AS document_type
|
||||||
FROM
|
FROM
|
||||||
documents
|
documents
|
||||||
@@ -301,31 +301,33 @@ func (p *Documents) LoadByOrganizationID(
|
|||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
WITH latest_versions AS (
|
WITH latest_versions AS (
|
||||||
SELECT DISTINCT ON (document_id) document_id, document_type
|
SELECT DISTINCT ON (document_id) document_id, title, document_type
|
||||||
FROM document_versions
|
FROM document_versions
|
||||||
ORDER BY document_id, major DESC, minor DESC
|
ORDER BY document_id, major DESC, minor DESC
|
||||||
|
),
|
||||||
|
base AS (
|
||||||
|
SELECT
|
||||||
|
documents.id,
|
||||||
|
documents.organization_id,
|
||||||
|
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.title, '') AS title,
|
||||||
|
COALESCE(lv.document_type, 'OTHER') AS document_type
|
||||||
|
FROM
|
||||||
|
documents
|
||||||
|
LEFT JOIN latest_versions lv ON lv.document_id = documents.id
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND documents.deleted_at IS NULL
|
||||||
|
AND documents.organization_id = @organization_id
|
||||||
|
AND %s
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT * FROM base WHERE %s
|
||||||
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 documents.deleted_at IS NULL
|
|
||||||
AND documents.organization_id = @organization_id
|
|
||||||
AND %s
|
|
||||||
AND %s
|
|
||||||
`
|
`
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||||
@@ -359,14 +361,13 @@ func (p *Documents) LoadAllByOrganizationID(
|
|||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
WITH latest_versions AS (
|
WITH latest_versions AS (
|
||||||
SELECT DISTINCT ON (document_id) document_id, document_type
|
SELECT DISTINCT ON (document_id) document_id, title, document_type
|
||||||
FROM document_versions
|
FROM document_versions
|
||||||
ORDER BY document_id, major DESC, minor DESC
|
ORDER BY document_id, major DESC, minor DESC
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
documents.id,
|
documents.id,
|
||||||
documents.organization_id,
|
documents.organization_id,
|
||||||
documents.title,
|
|
||||||
documents.current_published_major,
|
documents.current_published_major,
|
||||||
documents.current_published_minor,
|
documents.current_published_minor,
|
||||||
documents.trust_center_visibility,
|
documents.trust_center_visibility,
|
||||||
@@ -374,6 +375,7 @@ SELECT
|
|||||||
documents.archived_at,
|
documents.archived_at,
|
||||||
documents.created_at,
|
documents.created_at,
|
||||||
documents.updated_at,
|
documents.updated_at,
|
||||||
|
COALESCE(lv.title, '') AS title,
|
||||||
COALESCE(lv.document_type, 'OTHER') AS document_type
|
COALESCE(lv.document_type, 'OTHER') AS document_type
|
||||||
FROM
|
FROM
|
||||||
documents
|
documents
|
||||||
@@ -417,43 +419,48 @@ func (p *Documents) LoadPublishedByOrganizationID(
|
|||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
WITH latest_versions AS (
|
WITH latest_versions AS (
|
||||||
SELECT DISTINCT ON (document_id) document_id, document_type
|
SELECT DISTINCT ON (document_id) document_id, title, document_type
|
||||||
FROM document_versions
|
FROM document_versions
|
||||||
ORDER BY document_id, major DESC, minor DESC
|
ORDER BY document_id, major DESC, minor DESC
|
||||||
),
|
),
|
||||||
published_documents AS (
|
published_versions AS (
|
||||||
SELECT
|
SELECT
|
||||||
d.*,
|
dv.document_id,
|
||||||
dv.title AS published_title
|
dv.title AS published_title
|
||||||
FROM
|
FROM
|
||||||
documents d
|
document_versions dv
|
||||||
LEFT JOIN document_versions dv
|
INNER JOIN documents d
|
||||||
ON dv.document_id = d.id
|
ON dv.document_id = d.id
|
||||||
AND dv.major = d.current_published_major
|
AND dv.major = d.current_published_major
|
||||||
AND dv.minor = d.current_published_minor
|
AND dv.minor = d.current_published_minor
|
||||||
WHERE
|
WHERE
|
||||||
d.deleted_at IS NULL
|
d.deleted_at IS NULL
|
||||||
AND d.organization_id = @organization_id
|
AND d.organization_id = @organization_id
|
||||||
|
),
|
||||||
|
base AS (
|
||||||
|
SELECT
|
||||||
|
documents.id,
|
||||||
|
documents.organization_id,
|
||||||
|
documents.current_published_major,
|
||||||
|
documents.current_published_minor,
|
||||||
|
documents.trust_center_visibility,
|
||||||
|
documents.status,
|
||||||
|
documents.archived_at,
|
||||||
|
documents.created_at,
|
||||||
|
documents.updated_at,
|
||||||
|
COALESCE(pv.published_title, lv.title, '') AS title,
|
||||||
|
COALESCE(lv.document_type, 'OTHER') AS document_type
|
||||||
|
FROM
|
||||||
|
documents
|
||||||
|
LEFT JOIN latest_versions lv ON lv.document_id = documents.id
|
||||||
|
LEFT JOIN published_versions pv ON pv.document_id = documents.id
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND documents.deleted_at IS NULL
|
||||||
|
AND documents.organization_id = @organization_id
|
||||||
|
AND %s
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT * FROM base WHERE %s
|
||||||
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
|
|
||||||
AND %s
|
|
||||||
`
|
`
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||||
|
|
||||||
@@ -488,7 +495,6 @@ INSERT INTO
|
|||||||
tenant_id,
|
tenant_id,
|
||||||
id,
|
id,
|
||||||
organization_id,
|
organization_id,
|
||||||
title,
|
|
||||||
current_published_major,
|
current_published_major,
|
||||||
current_published_minor,
|
current_published_minor,
|
||||||
trust_center_visibility,
|
trust_center_visibility,
|
||||||
@@ -501,7 +507,6 @@ VALUES (
|
|||||||
@tenant_id,
|
@tenant_id,
|
||||||
@document_id,
|
@document_id,
|
||||||
@organization_id,
|
@organization_id,
|
||||||
@title,
|
|
||||||
@current_published_major,
|
@current_published_major,
|
||||||
@current_published_minor,
|
@current_published_minor,
|
||||||
@trust_center_visibility,
|
@trust_center_visibility,
|
||||||
@@ -516,7 +521,6 @@ VALUES (
|
|||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"document_id": p.ID,
|
"document_id": p.ID,
|
||||||
"organization_id": p.OrganizationID,
|
"organization_id": p.OrganizationID,
|
||||||
"title": p.Title,
|
|
||||||
"current_published_major": p.CurrentPublishedMajor,
|
"current_published_major": p.CurrentPublishedMajor,
|
||||||
"current_published_minor": p.CurrentPublishedMinor,
|
"current_published_minor": p.CurrentPublishedMinor,
|
||||||
"trust_center_visibility": p.TrustCenterVisibility,
|
"trust_center_visibility": p.TrustCenterVisibility,
|
||||||
@@ -575,7 +579,6 @@ func (p *Document) Update(
|
|||||||
UPDATE
|
UPDATE
|
||||||
documents
|
documents
|
||||||
SET
|
SET
|
||||||
title = @title,
|
|
||||||
current_published_major = @current_published_major,
|
current_published_major = @current_published_major,
|
||||||
current_published_minor = @current_published_minor,
|
current_published_minor = @current_published_minor,
|
||||||
trust_center_visibility = @trust_center_visibility,
|
trust_center_visibility = @trust_center_visibility,
|
||||||
@@ -592,7 +595,6 @@ WHERE
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"document_id": p.ID,
|
"document_id": p.ID,
|
||||||
"updated_at": time.Now(),
|
"updated_at": time.Now(),
|
||||||
"title": p.Title,
|
|
||||||
"current_published_major": p.CurrentPublishedMajor,
|
"current_published_major": p.CurrentPublishedMajor,
|
||||||
"current_published_minor": p.CurrentPublishedMinor,
|
"current_published_minor": p.CurrentPublishedMinor,
|
||||||
"trust_center_visibility": p.TrustCenterVisibility,
|
"trust_center_visibility": p.TrustCenterVisibility,
|
||||||
@@ -655,7 +657,7 @@ func (p *Documents) LoadByControlID(
|
|||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
WITH latest_versions AS (
|
WITH latest_versions AS (
|
||||||
SELECT DISTINCT ON (document_id) document_id, document_type
|
SELECT DISTINCT ON (document_id) document_id, title, document_type
|
||||||
FROM document_versions
|
FROM document_versions
|
||||||
ORDER BY document_id, major DESC, minor DESC
|
ORDER BY document_id, major DESC, minor DESC
|
||||||
),
|
),
|
||||||
@@ -665,24 +667,26 @@ scoped_documents AS (
|
|||||||
WHERE %s
|
WHERE %s
|
||||||
AND deleted_at IS NULL
|
AND deleted_at IS NULL
|
||||||
AND %s
|
AND %s
|
||||||
AND %s
|
),
|
||||||
|
base AS (
|
||||||
|
SELECT
|
||||||
|
sd.id,
|
||||||
|
sd.organization_id,
|
||||||
|
sd.current_published_major,
|
||||||
|
sd.current_published_minor,
|
||||||
|
sd.trust_center_visibility,
|
||||||
|
sd.status,
|
||||||
|
sd.archived_at,
|
||||||
|
sd.created_at,
|
||||||
|
sd.updated_at,
|
||||||
|
COALESCE(lv.title, '') AS title,
|
||||||
|
COALESCE(lv.document_type, 'OTHER') AS document_type
|
||||||
|
FROM scoped_documents sd
|
||||||
|
INNER JOIN controls_documents cp ON sd.id = cp.document_id
|
||||||
|
LEFT JOIN latest_versions lv ON lv.document_id = sd.id
|
||||||
|
WHERE cp.control_id = @control_id
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT * FROM base WHERE %s
|
||||||
scoped_documents.id,
|
|
||||||
scoped_documents.organization_id,
|
|
||||||
scoped_documents.title,
|
|
||||||
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,
|
|
||||||
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())
|
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||||
|
|
||||||
@@ -752,7 +756,7 @@ func (p *Documents) LoadByRiskID(
|
|||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
WITH latest_versions AS (
|
WITH latest_versions AS (
|
||||||
SELECT DISTINCT ON (document_id) document_id, document_type
|
SELECT DISTINCT ON (document_id) document_id, title, document_type
|
||||||
FROM document_versions
|
FROM document_versions
|
||||||
ORDER BY document_id, major DESC, minor DESC
|
ORDER BY document_id, major DESC, minor DESC
|
||||||
),
|
),
|
||||||
@@ -762,24 +766,26 @@ scoped_documents AS (
|
|||||||
WHERE %s
|
WHERE %s
|
||||||
AND deleted_at IS NULL
|
AND deleted_at IS NULL
|
||||||
AND %s
|
AND %s
|
||||||
AND %s
|
),
|
||||||
|
base AS (
|
||||||
|
SELECT
|
||||||
|
sd.id,
|
||||||
|
sd.organization_id,
|
||||||
|
sd.current_published_major,
|
||||||
|
sd.current_published_minor,
|
||||||
|
sd.trust_center_visibility,
|
||||||
|
sd.status,
|
||||||
|
sd.archived_at,
|
||||||
|
sd.created_at,
|
||||||
|
sd.updated_at,
|
||||||
|
COALESCE(lv.title, '') AS title,
|
||||||
|
COALESCE(lv.document_type, 'OTHER') AS document_type
|
||||||
|
FROM scoped_documents sd
|
||||||
|
INNER JOIN risks_documents rp ON sd.id = rp.document_id
|
||||||
|
LEFT JOIN latest_versions lv ON lv.document_id = sd.id
|
||||||
|
WHERE rp.risk_id = @risk_id
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT * FROM base WHERE %s
|
||||||
scoped_documents.id,
|
|
||||||
scoped_documents.organization_id,
|
|
||||||
scoped_documents.title,
|
|
||||||
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,
|
|
||||||
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())
|
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||||
|
|
||||||
@@ -849,7 +855,7 @@ func (p *Documents) LoadByMeasureID(
|
|||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
WITH latest_versions AS (
|
WITH latest_versions AS (
|
||||||
SELECT DISTINCT ON (document_id) document_id, document_type
|
SELECT DISTINCT ON (document_id) document_id, title, document_type
|
||||||
FROM document_versions
|
FROM document_versions
|
||||||
ORDER BY document_id, major DESC, minor DESC
|
ORDER BY document_id, major DESC, minor DESC
|
||||||
),
|
),
|
||||||
@@ -859,24 +865,26 @@ scoped_documents AS (
|
|||||||
WHERE %s
|
WHERE %s
|
||||||
AND deleted_at IS NULL
|
AND deleted_at IS NULL
|
||||||
AND %s
|
AND %s
|
||||||
AND %s
|
),
|
||||||
|
base AS (
|
||||||
|
SELECT
|
||||||
|
sd.id,
|
||||||
|
sd.organization_id,
|
||||||
|
sd.current_published_major,
|
||||||
|
sd.current_published_minor,
|
||||||
|
sd.trust_center_visibility,
|
||||||
|
sd.status,
|
||||||
|
sd.archived_at,
|
||||||
|
sd.created_at,
|
||||||
|
sd.updated_at,
|
||||||
|
COALESCE(lv.title, '') AS title,
|
||||||
|
COALESCE(lv.document_type, 'OTHER') AS document_type
|
||||||
|
FROM scoped_documents sd
|
||||||
|
INNER JOIN measures_documents md ON sd.id = md.document_id
|
||||||
|
LEFT JOIN latest_versions lv ON lv.document_id = sd.id
|
||||||
|
WHERE md.measure_id = @measure_id
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT * FROM base WHERE %s
|
||||||
scoped_documents.id,
|
|
||||||
scoped_documents.organization_id,
|
|
||||||
scoped_documents.title,
|
|
||||||
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,
|
|
||||||
COALESCE(lv.document_type, 'OTHER') AS document_type
|
|
||||||
FROM scoped_documents
|
|
||||||
INNER JOIN measures_documents md ON scoped_documents.id = md.document_id
|
|
||||||
LEFT JOIN latest_versions lv ON lv.document_id = scoped_documents.id
|
|
||||||
WHERE md.measure_id = @measure_id
|
|
||||||
`
|
`
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||||
|
|
||||||
|
|||||||
16
pkg/coredata/migrations/20260407T120000Z.sql
Normal file
16
pkg/coredata/migrations/20260407T120000Z.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
-- Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
--
|
||||||
|
-- Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
-- purpose with or without fee is hereby granted, provided that the above
|
||||||
|
-- copyright notice and this permission notice appear in all copies.
|
||||||
|
--
|
||||||
|
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
-- PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
-- TODO: drop the title column from documents.
|
||||||
|
ALTER TABLE documents ALTER COLUMN title DROP NOT NULL;
|
||||||
@@ -89,13 +89,13 @@ type (
|
|||||||
|
|
||||||
UpdateDocumentRequest struct {
|
UpdateDocumentRequest struct {
|
||||||
DocumentID gid.GID
|
DocumentID gid.GID
|
||||||
Title *string
|
|
||||||
TrustCenterVisibility *coredata.TrustCenterVisibility
|
TrustCenterVisibility *coredata.TrustCenterVisibility
|
||||||
DefaultApproverIDs *[]gid.GID
|
DefaultApproverIDs *[]gid.GID
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateDocumentVersionRequest struct {
|
UpdateDocumentVersionRequest struct {
|
||||||
ID gid.GID
|
ID gid.GID
|
||||||
|
Title *string
|
||||||
Content *string
|
Content *string
|
||||||
Classification *coredata.DocumentClassification
|
Classification *coredata.DocumentClassification
|
||||||
DocumentType *coredata.DocumentType
|
DocumentType *coredata.DocumentType
|
||||||
@@ -150,7 +150,6 @@ func (udr *UpdateDocumentRequest) Validate() error {
|
|||||||
v := validator.New()
|
v := validator.New()
|
||||||
|
|
||||||
v.Check(udr.DocumentID, "document_id", validator.Required(), validator.GID(coredata.DocumentEntityType))
|
v.Check(udr.DocumentID, "document_id", validator.Required(), validator.GID(coredata.DocumentEntityType))
|
||||||
v.Check(udr.Title, "title", validator.SafeTextNoNewLine(TitleMaxLength))
|
|
||||||
v.Check(udr.TrustCenterVisibility, "trust_center_visibility", validator.OneOfSlice(coredata.TrustCenterVisibilities()))
|
v.Check(udr.TrustCenterVisibility, "trust_center_visibility", validator.OneOfSlice(coredata.TrustCenterVisibilities()))
|
||||||
if udr.DefaultApproverIDs != nil {
|
if udr.DefaultApproverIDs != nil {
|
||||||
v.Check(len(*udr.DefaultApproverIDs), "default_approver_ids", validator.Max(100))
|
v.Check(len(*udr.DefaultApproverIDs), "default_approver_ids", validator.Max(100))
|
||||||
@@ -167,6 +166,7 @@ func (udvr *UpdateDocumentVersionRequest) Validate() error {
|
|||||||
v := validator.New()
|
v := validator.New()
|
||||||
|
|
||||||
v.Check(udvr.ID, "id", validator.Required(), validator.GID(coredata.DocumentVersionEntityType))
|
v.Check(udvr.ID, "id", validator.Required(), validator.GID(coredata.DocumentVersionEntityType))
|
||||||
|
v.Check(udvr.Title, "title", validator.SafeTextNoNewLine(TitleMaxLength))
|
||||||
v.Check(udvr.Classification, "classification", validator.OneOfSlice(coredata.DocumentClassifications()))
|
v.Check(udvr.Classification, "classification", validator.OneOfSlice(coredata.DocumentClassifications()))
|
||||||
v.Check(
|
v.Check(
|
||||||
udvr.Content,
|
udvr.Content,
|
||||||
@@ -584,7 +584,6 @@ func (s *DocumentService) Create(
|
|||||||
|
|
||||||
document := &coredata.Document{
|
document := &coredata.Document{
|
||||||
ID: documentID,
|
ID: documentID,
|
||||||
Title: req.Title,
|
|
||||||
TrustCenterVisibility: coredata.TrustCenterVisibilityNone,
|
TrustCenterVisibility: coredata.TrustCenterVisibilityNone,
|
||||||
Status: coredata.DocumentStatusActive,
|
Status: coredata.DocumentStatusActive,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
@@ -863,7 +862,9 @@ func (s *DocumentService) UpdateVersion(
|
|||||||
documentVersion.Content = content
|
documentVersion.Content = content
|
||||||
}
|
}
|
||||||
|
|
||||||
documentVersion.Title = document.Title
|
if req.Title != nil {
|
||||||
|
documentVersion.Title = *req.Title
|
||||||
|
}
|
||||||
if req.Classification != nil {
|
if req.Classification != nil {
|
||||||
documentVersion.Classification = *req.Classification
|
documentVersion.Classification = *req.Classification
|
||||||
}
|
}
|
||||||
@@ -1119,7 +1120,7 @@ func (s *DocumentService) CreateDraft(
|
|||||||
draftVersion.ID = draftVersionID
|
draftVersion.ID = draftVersionID
|
||||||
draftVersion.OrganizationID = document.OrganizationID
|
draftVersion.OrganizationID = document.OrganizationID
|
||||||
draftVersion.DocumentID = documentID
|
draftVersion.DocumentID = documentID
|
||||||
draftVersion.Title = document.Title
|
draftVersion.Title = latestVersion.Title
|
||||||
draftVersion.Major = latestVersion.Major
|
draftVersion.Major = latestVersion.Major
|
||||||
draftVersion.Minor = latestVersion.Minor + 1
|
draftVersion.Minor = latestVersion.Minor + 1
|
||||||
draftVersion.Classification = latestVersion.Classification
|
draftVersion.Classification = latestVersion.Classification
|
||||||
@@ -1716,10 +1717,6 @@ func (s *DocumentService) Update(
|
|||||||
return &ErrDocumentArchived{}
|
return &ErrDocumentArchived{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Title != nil {
|
|
||||||
document.Title = *req.Title
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.TrustCenterVisibility != nil {
|
if req.TrustCenterVisibility != nil {
|
||||||
document.TrustCenterVisibility = *req.TrustCenterVisibility
|
document.TrustCenterVisibility = *req.TrustCenterVisibility
|
||||||
}
|
}
|
||||||
@@ -1730,17 +1727,6 @@ func (s *DocumentService) Update(
|
|||||||
return fmt.Errorf("cannot update document: %w", err)
|
return fmt.Errorf("cannot update document: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
draftVersion := &coredata.DocumentVersion{}
|
|
||||||
err := draftVersion.LoadLatestVersion(ctx, tx, s.svc.scope, req.DocumentID)
|
|
||||||
if err == nil && draftVersion.Status == coredata.DocumentVersionStatusDraft {
|
|
||||||
draftVersion.Title = document.Title
|
|
||||||
draftVersion.UpdatedAt = now
|
|
||||||
|
|
||||||
if err := draftVersion.Update(ctx, tx, s.svc.scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot update draft version: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.DefaultApproverIDs != nil {
|
if req.DefaultApproverIDs != nil {
|
||||||
defaultApprovers := &coredata.DocumentDefaultApprovers{}
|
defaultApprovers := &coredata.DocumentDefaultApprovers{}
|
||||||
if err := defaultApprovers.MergeByDocumentID(ctx, tx, s.svc.scope, req.DocumentID, document.OrganizationID, *req.DefaultApproverIDs); err != nil {
|
if err := defaultApprovers.MergeByDocumentID(ctx, tx, s.svc.scope, req.DocumentID, document.OrganizationID, *req.DefaultApproverIDs); err != nil {
|
||||||
|
|||||||
@@ -2562,7 +2562,6 @@ type Evidence implements Node {
|
|||||||
|
|
||||||
type Document implements Node {
|
type Document implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
title: String!
|
|
||||||
description: String
|
description: String
|
||||||
currentPublishedMajor: Int
|
currentPublishedMajor: Int
|
||||||
currentPublishedMinor: Int
|
currentPublishedMinor: Int
|
||||||
@@ -4676,8 +4675,6 @@ input CreateDocumentInput {
|
|||||||
|
|
||||||
input UpdateDocumentInput {
|
input UpdateDocumentInput {
|
||||||
id: ID!
|
id: ID!
|
||||||
title: String
|
|
||||||
content: String
|
|
||||||
trustCenterVisibility: TrustCenterVisibility
|
trustCenterVisibility: TrustCenterVisibility
|
||||||
defaultApproverIds: [ID!]
|
defaultApproverIds: [ID!]
|
||||||
}
|
}
|
||||||
@@ -5945,6 +5942,7 @@ input DeleteDraftDocumentVersionInput {
|
|||||||
|
|
||||||
input UpdateDocumentVersionInput {
|
input UpdateDocumentVersionInput {
|
||||||
documentVersionId: ID!
|
documentVersionId: ID!
|
||||||
|
title: String
|
||||||
content: String
|
content: String
|
||||||
classification: DocumentClassification
|
classification: DocumentClassification
|
||||||
documentType: DocumentType
|
documentType: DocumentType
|
||||||
|
|||||||
@@ -75,8 +75,7 @@ func NewDocumentEdge(document *coredata.Document, orderBy coredata.DocumentOrder
|
|||||||
|
|
||||||
func NewDocument(document *coredata.Document) *Document {
|
func NewDocument(document *coredata.Document) *Document {
|
||||||
return &Document{
|
return &Document{
|
||||||
ID: document.ID,
|
ID: document.ID,
|
||||||
Title: document.Title,
|
|
||||||
Organization: &Organization{
|
Organization: &Organization{
|
||||||
ID: document.OrganizationID,
|
ID: document.OrganizationID,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5307,7 +5307,6 @@ func (r *mutationResolver) UpdateDocument(ctx context.Context, input types.Updat
|
|||||||
ctx,
|
ctx,
|
||||||
probo.UpdateDocumentRequest{
|
probo.UpdateDocumentRequest{
|
||||||
DocumentID: input.ID,
|
DocumentID: input.ID,
|
||||||
Title: input.Title,
|
|
||||||
TrustCenterVisibility: input.TrustCenterVisibility,
|
TrustCenterVisibility: input.TrustCenterVisibility,
|
||||||
DefaultApproverIDs: defaultApproverIDs,
|
DefaultApproverIDs: defaultApproverIDs,
|
||||||
},
|
},
|
||||||
@@ -6101,6 +6100,7 @@ func (r *mutationResolver) UpdateDocumentVersion(ctx context.Context, input type
|
|||||||
ctx,
|
ctx,
|
||||||
probo.UpdateDocumentVersionRequest{
|
probo.UpdateDocumentVersionRequest{
|
||||||
ID: input.DocumentVersionID,
|
ID: input.DocumentVersionID,
|
||||||
|
Title: input.Title,
|
||||||
Content: input.Content,
|
Content: input.Content,
|
||||||
Classification: input.Classification,
|
Classification: input.Classification,
|
||||||
DocumentType: input.DocumentType,
|
DocumentType: input.DocumentType,
|
||||||
|
|||||||
@@ -2119,7 +2119,6 @@ func (r *Resolver) UpdateDocumentTool(ctx context.Context, req *mcp.CallToolRequ
|
|||||||
ctx,
|
ctx,
|
||||||
probo.UpdateDocumentRequest{
|
probo.UpdateDocumentRequest{
|
||||||
DocumentID: input.ID,
|
DocumentID: input.ID,
|
||||||
Title: input.Title,
|
|
||||||
TrustCenterVisibility: input.TrustCenterVisibility,
|
TrustCenterVisibility: input.TrustCenterVisibility,
|
||||||
DefaultApproverIDs: defaultApproverIDs,
|
DefaultApproverIDs: defaultApproverIDs,
|
||||||
},
|
},
|
||||||
@@ -2224,6 +2223,7 @@ func (r *Resolver) UpdateDocumentVersionTool(ctx context.Context, req *mcp.CallT
|
|||||||
ctx,
|
ctx,
|
||||||
probo.UpdateDocumentVersionRequest{
|
probo.UpdateDocumentVersionRequest{
|
||||||
ID: input.DocumentVersionID,
|
ID: input.DocumentVersionID,
|
||||||
|
Title: input.Title,
|
||||||
Content: content,
|
Content: content,
|
||||||
Classification: input.Classification,
|
Classification: input.Classification,
|
||||||
DocumentType: input.DocumentType,
|
DocumentType: input.DocumentType,
|
||||||
|
|||||||
@@ -5243,7 +5243,6 @@ components:
|
|||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- organization_id
|
- organization_id
|
||||||
- title
|
|
||||||
- trust_center_visibility
|
- trust_center_visibility
|
||||||
- status
|
- status
|
||||||
- created_at
|
- created_at
|
||||||
@@ -5255,9 +5254,6 @@ components:
|
|||||||
organization_id:
|
organization_id:
|
||||||
$ref: "#/components/schemas/GID"
|
$ref: "#/components/schemas/GID"
|
||||||
description: Organization ID
|
description: Organization ID
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
description: Document title
|
|
||||||
current_published_major:
|
current_published_major:
|
||||||
type:
|
type:
|
||||||
- integer
|
- integer
|
||||||
@@ -5522,9 +5518,6 @@ components:
|
|||||||
id:
|
id:
|
||||||
$ref: "#/components/schemas/GID"
|
$ref: "#/components/schemas/GID"
|
||||||
description: Document ID
|
description: Document ID
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
description: Document title
|
|
||||||
trust_center_visibility:
|
trust_center_visibility:
|
||||||
$ref: "#/components/schemas/TrustCenterVisibility"
|
$ref: "#/components/schemas/TrustCenterVisibility"
|
||||||
description: Trust center visibility
|
description: Trust center visibility
|
||||||
@@ -5655,6 +5648,9 @@ components:
|
|||||||
document_version_id:
|
document_version_id:
|
||||||
$ref: "#/components/schemas/GID"
|
$ref: "#/components/schemas/GID"
|
||||||
description: Document version ID
|
description: Document version ID
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
description: Document version title
|
||||||
content:
|
content:
|
||||||
type: string
|
type: string
|
||||||
description: Document content in markdown format
|
description: Document content in markdown format
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ func NewDocument(d *coredata.Document) *Document {
|
|||||||
return &Document{
|
return &Document{
|
||||||
ID: d.ID,
|
ID: d.ID,
|
||||||
OrganizationID: d.OrganizationID,
|
OrganizationID: d.OrganizationID,
|
||||||
Title: d.Title,
|
|
||||||
CurrentPublishedMajor: d.CurrentPublishedMajor,
|
CurrentPublishedMajor: d.CurrentPublishedMajor,
|
||||||
CurrentPublishedMinor: d.CurrentPublishedMinor,
|
CurrentPublishedMinor: d.CurrentPublishedMinor,
|
||||||
TrustCenterVisibility: d.TrustCenterVisibility,
|
TrustCenterVisibility: d.TrustCenterVisibility,
|
||||||
|
|||||||
Reference in New Issue
Block a user