Change document version

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-06-03 14:55:54 -07:00
parent 21e7891f9b
commit b54cb6d3fc
22 changed files with 1247 additions and 146 deletions

View File

@@ -25,7 +25,7 @@ import type { ShowDocumentViewQuery } from "./__generated__/ShowDocumentViewQuer
import { ShowDocumentViewPublishMutation } from "./__generated__/ShowDocumentViewPublishMutation.graphql";
import { ShowDocumentViewCreateDraftMutation } from "./__generated__/ShowDocumentViewCreateDraftMutation.graphql";
import { ShowDocumentViewUpdateDocumentMutation } from "./__generated__/ShowDocumentViewUpdateDocumentMutation.graphql";
import { ShowDocumentViewUpdateDocumentTypeMutation } from "./__generated__/ShowDocumentViewUpdateDocumentTypeMutation.graphql";
import { ShowDocumentViewGenerateChangelogMutation } from "./__generated__/ShowDocumentViewGenerateChangelogMutation.graphql";
import type { DocumentType } from "./__generated__/DocumentListViewCreateMutation.graphql";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
@@ -90,6 +90,10 @@ const documentViewQuery = graphql`
content
changelog
publishedAt
title
owner {
fullName
}
publishedBy {
fullName
}
@@ -114,6 +118,7 @@ const publishDocumentVersionMutation = graphql`
id
status
publishedAt
changelog
publishedBy {
fullName
}
@@ -144,6 +149,7 @@ const updateDocumentMutation = graphql`
document {
id
documentType
title
owner {
id
fullName
@@ -154,6 +160,14 @@ const updateDocumentMutation = graphql`
}
`;
const generateChangelogMutation = graphql`
mutation ShowDocumentViewGenerateChangelogMutation($input: GenerateDocumentChangelogInput!) {
generateDocumentChangelog(input: $input) {
changelog
}
}
`;
function ShowDocumentContent({
queryRef,
}: {
@@ -176,8 +190,18 @@ function ShowDocumentContent({
const [isSignaturesModalOpen, setIsSignaturesModalOpen] = useState(false);
const [isEditingOwner, setIsEditingOwner] = useState(false);
const [isEditingType, setIsEditingType] = useState(false);
const [isEditingTitle, setIsEditingTitle] = useState(false);
const [editedTitle, setEditedTitle] = useState(documentValue.title || '');
const [isPublishDialogOpen, setIsPublishDialogOpen] = useState(false);
const [publishChangelog, setPublishChangelog] = useState('');
const [isGeneratingChangelog, setIsGeneratingChangelog] = useState(false);
const printContentRef = useRef<HTMLDivElement>(null);
// Keep editedTitle in sync with documentValue.title
useEffect(() => {
setEditedTitle(documentValue.title || '');
}, [documentValue.title]);
const [publishDraft, isPublishInFlight] =
useMutation<ShowDocumentViewPublishMutation>(publishDocumentVersionMutation);
const [createDraft, isCreateDraftInFlight] =
@@ -186,6 +210,7 @@ function ShowDocumentContent({
);
const [updateDocument, isUpdatingDocument] =
useMutation<ShowDocumentViewUpdateDocumentMutation>(updateDocumentMutation);
const [generateChangelog] = useMutation<ShowDocumentViewGenerateChangelogMutation>(generateChangelogMutation);
const latestVersionEdge = documentValue.latestVersion?.edges[0];
const latestVersionNode = latestVersionEdge?.node;
@@ -300,12 +325,57 @@ function ShowDocumentContent({
// Navigate to publish flow
const handlePublish = useCallback(() => {
setIsPublishDialogOpen(true);
setPublishChangelog("");
// Generate changelog
setIsGeneratingChangelog(true);
generateChangelog({
variables: {
input: {
documentId: documentValue.id,
},
},
onCompleted: (response, errors) => {
setIsGeneratingChangelog(false);
if (errors) {
toast({
title: "Error",
description: errors[0]?.message || "An unknown error occurred",
variant: "destructive",
});
return;
}
const generatedChangelog = response.generateDocumentChangelog.changelog;
setPublishChangelog(prev => {
if (prev.trim()) {
return `${prev}\n${generatedChangelog}`;
}
return generatedChangelog;
});
},
onError: (error) => {
setIsGeneratingChangelog(false);
toast({
title: "Error",
description: error.message || "An unknown error occurred",
variant: "destructive",
});
},
});
}, [documentValue.id, generateChangelog, toast]);
// Confirm publish
const confirmPublish = useCallback(() => {
if (!documentValue.id) return;
publishDraft({
variables: {
input: {
documentId: documentValue.id,
changelog: publishChangelog,
},
},
onCompleted: (_, errors) => {
@@ -323,6 +393,7 @@ function ShowDocumentContent({
description: `The document has been published successfully`,
});
setIsPublishDialogOpen(false);
// Reload the query to refresh the data
loadQuery({ documentId: documentValue.id, organizationId: organizationId! });
},
@@ -334,7 +405,7 @@ function ShowDocumentContent({
});
},
});
}, [documentValue.id, publishDraft, toast, loadQuery]);
}, [documentValue.id, publishDraft, toast, loadQuery, publishChangelog, organizationId]);
// Open version history modal
const handleVersionHistoryClick = useCallback(() => {
@@ -658,7 +729,84 @@ function ShowDocumentContent({
<div className="bg-gray-50 rounded-lg border border-solid-b shadow-sm p-6 mb-4">
<div className="grid grid-cols-2 gap-y-3">
<div>
<span className="font-medium">Document Title:</span> {documentValue.title}
<div className="flex items-center gap-2">
<span className="font-medium">Document Title:</span>
{isEditingTitle ? (
<div className="flex items-center gap-2">
<input
type="text"
className="h-9 w-[250px] rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors"
value={editedTitle}
onChange={(e) => setEditedTitle(e.target.value)}
placeholder="Enter document title"
/>
<Button
variant="outline"
size="sm"
onClick={() => {
if (editedTitle.trim()) {
updateDocument({
variables: {
input: {
id: documentValue.id,
title: editedTitle.trim(),
},
},
onCompleted: (_, errors) => {
if (errors) {
toast({
title: "Error updating title",
description: errors[0]?.message || "An unknown error occurred",
variant: "destructive",
});
return;
}
toast({
title: "Success",
description: "Document title updated successfully",
});
setIsEditingTitle(false);
loadQuery({ documentId: documentValue.id, organizationId: organizationId! });
},
onError: (error) => {
toast({
title: "Error updating title",
description: error.message || "An unknown error occurred",
variant: "destructive",
});
},
});
}
}}
disabled={!editedTitle.trim() || isUpdatingDocument}
>
Save
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setIsEditingTitle(false);
setEditedTitle(documentValue.title || '');
}}
disabled={isUpdatingDocument}
>
Cancel
</Button>
</div>
) : (
<div className="flex items-center gap-2">
<span>{documentValue.title}</span>
<Button
variant="outline"
size="sm"
onClick={() => setIsEditingTitle(true)}
>
Change
</Button>
</div>
)}
</div>
</div>
<div>
<span className="font-medium">Version:</span> {latestVersionNode.version || "N/A"}
@@ -826,6 +974,50 @@ function ShowDocumentContent({
</DialogFooter>
</DialogContent>
</Dialog>
{/* Publish Confirmation Dialog */}
<Dialog open={isPublishDialogOpen} onOpenChange={setIsPublishDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Publish Document</DialogTitle>
<DialogDescription>
Please review and edit the changelog before publishing the document.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<Label htmlFor="changelog">Changelog</Label>
<textarea
id="changelog"
className="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
value={publishChangelog}
onChange={(e) => setPublishChangelog(e.target.value)}
placeholder="Enter changelog message"
/>
{isGeneratingChangelog && (
<div className="mt-2 flex items-center gap-2 text-sm text-muted-foreground">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span>Generating changelog...</span>
</div>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setIsPublishDialogOpen(false)}
disabled={isPublishInFlight}
>
Cancel
</Button>
<Button
variant="default"
onClick={confirmPublish}
disabled={isPublishInFlight || !publishChangelog.trim()}
>
{isPublishInFlight ? "Publishing..." : "Publish"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</PageTemplate>
);
}
@@ -834,10 +1026,26 @@ export default function ShowDocumentView() {
const [queryRef, loadQuery] =
useQueryLoader<ShowDocumentViewQuery>(documentViewQuery);
const { documentId, organizationId } = useParams();
const { toast } = useToast();
const navigate = useNavigate();
useEffect(() => {
loadQuery({ documentId: documentId!, organizationId: organizationId! });
}, [loadQuery, documentId, organizationId]);
if (!organizationId || !documentId) {
toast({
title: "Error",
description: "Invalid document or organization ID",
variant: "destructive",
});
navigate("/");
return;
}
loadQuery({ documentId, organizationId });
}, [loadQuery, documentId, organizationId, toast, navigate]);
if (!organizationId || !documentId) {
return null;
}
if (!queryRef) {
return <ShowDocumentViewSkeleton />;

View File

@@ -22,9 +22,13 @@ export const documentVersionsFragment = graphql`
version
status
content
title
changelog
publishedAt
updatedAt
owner {
fullName
}
publishedBy {
fullName
}
@@ -122,6 +126,14 @@ export function VersionHistoryModal({
•{" "}
{formatDateTime(version.publishedAt || version.updatedAt)}
</div>
{version.changelog && (
<div className="text-xs text-tertiary mt-1">
Changelog: {version.changelog}
</div>
)}
<div className="text-xs text-tertiary mt-1">
Owner: {version.owner?.fullName || "Unknown"}
</div>
</div>
</div>
))}
@@ -130,8 +142,12 @@ export function VersionHistoryModal({
{/* Content Area */}
<div className="flex-1 p-6 relative overflow-y-auto">
<h2 className="text-2xl font-semibold mb-6">{data.title}</h2>
{selectedVersion && (
<h2 className="text-2xl font-semibold mb-6">
{versions.find((v) => v.version === selectedVersion)
?.title || data.title}
</h2>
)}
<div className="prose prose-olive max-w-none">
{selectedVersion && (
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>

View File

@@ -0,0 +1,92 @@
/**
* @generated SignedSource<<7743b633410bdb28ce017cc0799f2df4>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type GenerateDocumentChangelogInput = {
documentId: string;
};
export type ShowDocumentViewGenerateChangelogMutation$variables = {
input: GenerateDocumentChangelogInput;
};
export type ShowDocumentViewGenerateChangelogMutation$data = {
readonly generateDocumentChangelog: {
readonly changelog: string;
};
};
export type ShowDocumentViewGenerateChangelogMutation = {
response: ShowDocumentViewGenerateChangelogMutation$data;
variables: ShowDocumentViewGenerateChangelogMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "GenerateDocumentChangelogPayload",
"kind": "LinkedField",
"name": "generateDocumentChangelog",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "changelog",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "ShowDocumentViewGenerateChangelogMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "ShowDocumentViewGenerateChangelogMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "8051b5412b1d2d73a7fe686890e86f41",
"id": null,
"metadata": {},
"name": "ShowDocumentViewGenerateChangelogMutation",
"operationKind": "mutation",
"text": "mutation ShowDocumentViewGenerateChangelogMutation(\n $input: GenerateDocumentChangelogInput!\n) {\n generateDocumentChangelog(input: $input) {\n changelog\n }\n}\n"
}
};
})();
(node as any).hash = "60be598c9c528b2173be7be580057ea7";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<91076d34daad785f7fce9f5a2cbc4018>>
* @generated SignedSource<<f67241da996f7e38c84c64b99084ed97>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -11,6 +11,7 @@
import { ConcreteRequest } from 'relay-runtime';
export type DocumentStatus = "DRAFT" | "PUBLISHED";
export type PublishDocumentVersionInput = {
changelog?: string | null | undefined;
documentId: string;
};
export type ShowDocumentViewPublishMutation$variables = {
@@ -23,6 +24,7 @@ export type ShowDocumentViewPublishMutation$data = {
readonly id: string;
};
readonly documentVersion: {
readonly changelog: string;
readonly id: string;
readonly publishedAt: string | null | undefined;
readonly publishedBy: {
@@ -93,6 +95,13 @@ v5 = {
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "changelog",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
@@ -126,6 +135,7 @@ return {
(v2/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
{
"alias": null,
"args": null,
@@ -134,7 +144,7 @@ return {
"name": "publishedBy",
"plural": false,
"selections": [
(v6/*: any*/)
(v7/*: any*/)
],
"storageKey": null
}
@@ -174,6 +184,7 @@ return {
(v2/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
{
"alias": null,
"args": null,
@@ -182,7 +193,7 @@ return {
"name": "publishedBy",
"plural": false,
"selections": [
(v6/*: any*/),
(v7/*: any*/),
(v2/*: any*/)
],
"storageKey": null
@@ -196,16 +207,16 @@ return {
]
},
"params": {
"cacheID": "794539aab3ddce5d004e72d10b980ca4",
"cacheID": "9e097de4f6f61bd4762e57b4863c573b",
"id": null,
"metadata": {},
"name": "ShowDocumentViewPublishMutation",
"operationKind": "mutation",
"text": "mutation ShowDocumentViewPublishMutation(\n $input: PublishDocumentVersionInput!\n) {\n publishDocumentVersion(input: $input) {\n document {\n id\n currentPublishedVersion\n }\n documentVersion {\n id\n status\n publishedAt\n publishedBy {\n fullName\n id\n }\n }\n }\n}\n"
"text": "mutation ShowDocumentViewPublishMutation(\n $input: PublishDocumentVersionInput!\n) {\n publishDocumentVersion(input: $input) {\n document {\n id\n currentPublishedVersion\n }\n documentVersion {\n id\n status\n publishedAt\n changelog\n publishedBy {\n fullName\n id\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "66e590251b58966c2ac09625759332f7";
(node as any).hash = "9b2033c7f39d178fb9a25828ade6af3c";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<bf950636055ed83dd68cd6e513a4d83e>>
* @generated SignedSource<<edd3d7d537e2ed956853a8be083189fa>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -30,11 +30,15 @@ export type ShowDocumentViewQuery$data = {
readonly content: string;
readonly createdAt: string;
readonly id: string;
readonly owner: {
readonly fullName: string;
};
readonly publishedAt: string | null | undefined;
readonly publishedBy: {
readonly fullName: string;
} | null | undefined;
readonly status: DocumentStatus;
readonly title: string;
readonly updatedAt: string;
readonly version: number;
};
@@ -212,20 +216,23 @@ v19 = {
"name": "publishedAt",
"storageKey": null
},
v20 = {
v20 = [
(v11/*: any*/)
],
v21 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v21 = {
v22 = {
"kind": "Literal",
"name": "first",
"value": 100
},
v22 = [
(v21/*: any*/),
v23 = [
(v22/*: any*/),
{
"kind": "Literal",
"name": "orderBy",
@@ -235,14 +242,14 @@ v22 = [
}
}
],
v23 = {
v24 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
},
v24 = {
v25 = {
"alias": null,
"args": null,
"concreteType": "PageInfo",
@@ -267,23 +274,33 @@ v24 = {
],
"storageKey": null
},
v25 = [
v26 = [
(v11/*: any*/),
(v4/*: any*/)
],
v26 = {
v27 = {
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "publishedBy",
"plural": false,
"selections": (v25/*: any*/),
"selections": (v26/*: any*/),
"storageKey": null
},
v27 = [
(v21/*: any*/)
];
v28 = [
(v22/*: any*/)
],
v29 = {
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "owner",
"plural": false,
"selections": (v26/*: any*/),
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
@@ -374,6 +391,17 @@ return {
(v17/*: any*/),
(v18/*: any*/),
(v19/*: any*/),
(v5/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "owner",
"plural": false,
"selections": (v20/*: any*/),
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -381,9 +409,7 @@ return {
"kind": "LinkedField",
"name": "publishedBy",
"plural": false,
"selections": [
(v11/*: any*/)
],
"selections": (v20/*: any*/),
"storageKey": null
},
(v7/*: any*/),
@@ -422,7 +448,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v20/*: any*/),
(v21/*: any*/),
(v4/*: any*/),
{
"kind": "InlineFragment",
@@ -430,7 +456,7 @@ return {
(v2/*: any*/),
{
"alias": null,
"args": (v22/*: any*/),
"args": (v23/*: any*/),
"concreteType": "PeopleConnection",
"kind": "LinkedField",
"name": "peoples",
@@ -455,21 +481,21 @@ return {
(v4/*: any*/),
(v11/*: any*/),
(v12/*: any*/),
(v20/*: any*/)
(v21/*: any*/)
],
"storageKey": null
},
(v23/*: any*/)
(v24/*: any*/)
],
"storageKey": null
},
(v24/*: any*/)
(v25/*: any*/)
],
"storageKey": "peoples(first:100,orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})"
},
{
"alias": null,
"args": (v22/*: any*/),
"args": (v23/*: any*/),
"filters": [
"orderBy"
],
@@ -493,7 +519,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v20/*: any*/),
(v21/*: any*/),
(v4/*: any*/),
{
"kind": "InlineFragment",
@@ -540,10 +566,10 @@ return {
(v16/*: any*/),
(v19/*: any*/),
(v8/*: any*/),
(v26/*: any*/),
(v27/*: any*/),
{
"alias": null,
"args": (v27/*: any*/),
"args": (v28/*: any*/),
"concreteType": "DocumentVersionSignatureConnection",
"kind": "LinkedField",
"name": "signatures",
@@ -594,7 +620,7 @@ return {
"kind": "LinkedField",
"name": "signedBy",
"plural": false,
"selections": (v25/*: any*/),
"selections": (v26/*: any*/),
"storageKey": null
},
{
@@ -604,24 +630,24 @@ return {
"kind": "LinkedField",
"name": "requestedBy",
"plural": false,
"selections": (v25/*: any*/),
"selections": (v26/*: any*/),
"storageKey": null
},
(v20/*: any*/)
(v21/*: any*/)
],
"storageKey": null
},
(v23/*: any*/)
(v24/*: any*/)
],
"storageKey": null
},
(v24/*: any*/)
(v25/*: any*/)
],
"storageKey": "signatures(first:100)"
},
{
"alias": null,
"args": (v27/*: any*/),
"args": (v28/*: any*/),
"filters": null,
"handle": "connection",
"key": "SignaturesModal_documentVersions_signatures",
@@ -671,10 +697,12 @@ return {
(v15/*: any*/),
(v16/*: any*/),
(v17/*: any*/),
(v5/*: any*/),
(v18/*: any*/),
(v19/*: any*/),
(v8/*: any*/),
(v26/*: any*/)
(v29/*: any*/),
(v27/*: any*/)
],
"storageKey": null
}
@@ -714,7 +742,9 @@ return {
(v17/*: any*/),
(v18/*: any*/),
(v19/*: any*/),
(v26/*: any*/),
(v5/*: any*/),
(v29/*: any*/),
(v27/*: any*/),
(v7/*: any*/),
(v8/*: any*/)
],
@@ -736,16 +766,16 @@ return {
]
},
"params": {
"cacheID": "a1b25325fb4a764d130253f86435efcb",
"cacheID": "7e1dfac8673eeb68f1d3821db3ca95c2",
"id": null,
"metadata": {},
"name": "ShowDocumentViewQuery",
"operationKind": "query",
"text": "query ShowDocumentViewQuery(\n $documentId: ID!\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n name\n ...PeopleSelector_organization\n }\n id\n }\n node(id: $documentId) {\n __typename\n id\n ... on Document {\n title\n description\n createdAt\n updatedAt\n currentPublishedVersion\n documentType\n owner {\n id\n fullName\n primaryEmailAddress\n }\n ...SignaturesModal_documentVersions\n ...VersionHistoryModal_documentVersions\n latestVersion: versions(first: 1) {\n edges {\n node {\n id\n version\n status\n content\n changelog\n publishedAt\n publishedBy {\n fullName\n id\n }\n createdAt\n updatedAt\n }\n }\n }\n }\n }\n}\n\nfragment PeopleSelector_organization on Organization {\n id\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment SignaturesModal_documentVersions on Document {\n title\n documentVersions: versions(first: 10) {\n edges {\n node {\n id\n version\n status\n publishedAt\n updatedAt\n publishedBy {\n fullName\n id\n }\n signatures(first: 100) {\n edges {\n node {\n id\n state\n signedAt\n requestedAt\n signedBy {\n fullName\n id\n }\n requestedBy {\n fullName\n id\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n }\n}\n\nfragment VersionHistoryModal_documentVersions on Document {\n title\n owner {\n fullName\n id\n }\n versionHistory: versions(first: 20) {\n edges {\n node {\n id\n version\n status\n content\n changelog\n publishedAt\n updatedAt\n publishedBy {\n fullName\n id\n }\n }\n }\n }\n}\n"
"text": "query ShowDocumentViewQuery(\n $documentId: ID!\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n name\n ...PeopleSelector_organization\n }\n id\n }\n node(id: $documentId) {\n __typename\n id\n ... on Document {\n title\n description\n createdAt\n updatedAt\n currentPublishedVersion\n documentType\n owner {\n id\n fullName\n primaryEmailAddress\n }\n ...SignaturesModal_documentVersions\n ...VersionHistoryModal_documentVersions\n latestVersion: versions(first: 1) {\n edges {\n node {\n id\n version\n status\n content\n changelog\n publishedAt\n title\n owner {\n fullName\n id\n }\n publishedBy {\n fullName\n id\n }\n createdAt\n updatedAt\n }\n }\n }\n }\n }\n}\n\nfragment PeopleSelector_organization on Organization {\n id\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment SignaturesModal_documentVersions on Document {\n title\n documentVersions: versions(first: 10) {\n edges {\n node {\n id\n version\n status\n publishedAt\n updatedAt\n publishedBy {\n fullName\n id\n }\n signatures(first: 100) {\n edges {\n node {\n id\n state\n signedAt\n requestedAt\n signedBy {\n fullName\n id\n }\n requestedBy {\n fullName\n id\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n }\n}\n\nfragment VersionHistoryModal_documentVersions on Document {\n title\n owner {\n fullName\n id\n }\n versionHistory: versions(first: 20) {\n edges {\n node {\n id\n version\n status\n content\n title\n changelog\n publishedAt\n updatedAt\n owner {\n fullName\n id\n }\n publishedBy {\n fullName\n id\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "96c84ddc6ada95da2302068bae68f825";
(node as any).hash = "41461d6b97a3672ae58a36fbb5615489";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<f884c800d6c27056d14eec073a481151>>
* @generated SignedSource<<5cf1d2c84b94cf4c7d35965f67b36191>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -31,6 +31,7 @@ export type ShowDocumentViewUpdateDocumentMutation$data = {
readonly id: string;
readonly primaryEmailAddress: string;
};
readonly title: string;
};
};
};
@@ -85,6 +86,13 @@ v2 = [
"name": "documentType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -136,16 +144,16 @@ return {
"selections": (v2/*: any*/)
},
"params": {
"cacheID": "62cd235bd1903d80012364fbc3358054",
"cacheID": "81d866e2f2de8bc1d7d440bfb2c66144",
"id": null,
"metadata": {},
"name": "ShowDocumentViewUpdateDocumentMutation",
"operationKind": "mutation",
"text": "mutation ShowDocumentViewUpdateDocumentMutation(\n $input: UpdateDocumentInput!\n) {\n updateDocument(input: $input) {\n document {\n id\n documentType\n owner {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n}\n"
"text": "mutation ShowDocumentViewUpdateDocumentMutation(\n $input: UpdateDocumentInput!\n) {\n updateDocument(input: $input) {\n document {\n id\n documentType\n title\n owner {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "094ff9193be9f023152588380f5114b5";
(node as any).hash = "acaf0d38623ccc096f19c9d6c3e05c69";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<a88582302d627481e4214530f287e233>>
* @generated SignedSource<<00c4778ab72567e2c573d98bdbbc4c5f>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -22,11 +22,15 @@ export type VersionHistoryModal_documentVersions$data = {
readonly changelog: string;
readonly content: string;
readonly id: string;
readonly owner: {
readonly fullName: string;
};
readonly publishedAt: string | null | undefined;
readonly publishedBy: {
readonly fullName: string;
} | null | undefined;
readonly status: DocumentStatus;
readonly title: string;
readonly updatedAt: string;
readonly version: number;
};
@@ -40,7 +44,14 @@ export type VersionHistoryModal_documentVersions$key = {
};
const node: ReaderFragment = (function(){
var v0 = [
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
v1 = [
{
"alias": null,
"args": null,
@@ -48,30 +59,25 @@ var v0 = [
"name": "fullName",
"storageKey": null
}
];
],
v2 = {
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "owner",
"plural": false,
"selections": (v1/*: any*/),
"storageKey": null
};
return {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "VersionHistoryModal_documentVersions",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "owner",
"plural": false,
"selections": (v0/*: any*/),
"storageKey": null
},
(v0/*: any*/),
(v2/*: any*/),
{
"alias": "versionHistory",
"args": [
@@ -130,6 +136,7 @@ return {
"name": "content",
"storageKey": null
},
(v0/*: any*/),
{
"alias": null,
"args": null,
@@ -151,6 +158,7 @@ return {
"name": "updatedAt",
"storageKey": null
},
(v2/*: any*/),
{
"alias": null,
"args": null,
@@ -158,7 +166,7 @@ return {
"kind": "LinkedField",
"name": "publishedBy",
"plural": false,
"selections": (v0/*: any*/),
"selections": (v1/*: any*/),
"storageKey": null
}
],
@@ -176,6 +184,6 @@ return {
};
})();
(node as any).hash = "ffcba061a60a144c663de41a48e784d1";
(node as any).hash = "344e822be4a3eb20d147ecd1c3ea7ced";
export default node;

41
pkg/agents/agents.go Normal file
View File

@@ -0,0 +1,41 @@
// Copyright (c) 2025 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.
package agents
import (
"github.com/openai/openai-go"
"github.com/openai/openai-go/option"
"go.gearno.de/kit/log"
)
type (
Agent struct {
l *log.Logger
cfg Config
client *openai.Client
}
Config struct {
OpenAIAPIKey string
Temperature float64
ModelName string
}
)
func NewAgent(l *log.Logger, cfg Config) *Agent {
client := openai.NewClient(option.WithAPIKey(cfg.OpenAIAPIKey))
return &Agent{l: l, cfg: cfg, client: &client}
}

View File

@@ -0,0 +1,70 @@
// Copyright (c) 2025 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.
package agents
import (
"context"
"fmt"
"github.com/openai/openai-go"
"github.com/openai/openai-go/packages/param"
)
const (
changelogGeneratorSystemPrompt = `
# Role:You are an assistant that creates clear and concise changelogs.
# Objective
Given two versions of a document — the "old version" and the "new version" — identify and summarize all meaningful changes between them.
Focus on additions, deletions, modifications, and restructuring.
# Response Format
Respond with simple and short phrases that describe the changes, if possible use a single phrase.
# Change types
Change types can include: "Added", "Removed", "Updated", "Reworded", "Reorganized", "Fixed", etc.
# SOP
- Be objective and neutral in tone.
- Do not comment on the quality of the change.
- Use the language of the document.
**Example output format:**
Respond ONLY with the phrase that describes the changes. No explanation, no markdown, no preamble. Like this:
Added Clause about sharing personal information with trusted partners
`
)
func (a *Agent) GenerateChangelog(ctx context.Context, oldContent string, newContent string) (*string, error) {
model := openai.ChatModel(a.cfg.ModelName)
chatCompletion, err := a.client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
Messages: []openai.ChatCompletionMessageParamUnion{
openai.SystemMessage(changelogGeneratorSystemPrompt),
openai.UserMessage(fmt.Sprintf(`Old content: %s`, oldContent)),
openai.UserMessage(fmt.Sprintf(`New content: %s`, newContent)),
},
Model: model,
Temperature: param.NewOpt(a.cfg.Temperature),
})
if err != nil {
return nil, fmt.Errorf("failed to parse vendor info: %w", err)
}
if len(chatCompletion.Choices) == 0 {
return nil, fmt.Errorf("no completion choices returned from API")
}
return &chatCompletion.Choices[0].Message.Content, nil
}

View File

@@ -20,24 +20,10 @@ import (
"fmt"
"github.com/openai/openai-go"
"github.com/openai/openai-go/option"
"github.com/openai/openai-go/packages/param"
"go.gearno.de/kit/log"
)
type (
VendorAssessment struct {
l *log.Logger
cfg Config
client *openai.Client
}
Config struct {
OpenAIAPIKey string
Temperature float64
ModelName string
}
vendorInfo struct {
Name string `json:"name"`
Description string `json:"description"`
@@ -58,7 +44,7 @@ type (
)
const (
systemPrompt = `
assessVendorSystemPrompt = `
# Role: You are a compliance assistant.
# Objective
@@ -136,21 +122,15 @@ const (
`
)
func NewVendorAssessment(l *log.Logger, cfg Config) *VendorAssessment {
client := openai.NewClient(option.WithAPIKey(cfg.OpenAIAPIKey))
return &VendorAssessment{l: l, cfg: cfg, client: &client}
}
func (va *VendorAssessment) Fetch(ctx context.Context, websiteURL string) (*vendorInfo, error) {
model := openai.ChatModel(va.cfg.ModelName)
chatCompletion, err := va.client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
func (a *Agent) AssessVendor(ctx context.Context, websiteURL string) (*vendorInfo, error) {
model := openai.ChatModel(a.cfg.ModelName)
chatCompletion, err := a.client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
Messages: []openai.ChatCompletionMessageParamUnion{
openai.SystemMessage(systemPrompt),
openai.SystemMessage(assessVendorSystemPrompt),
openai.UserMessage(websiteURL),
},
Model: model,
Temperature: param.NewOpt(va.cfg.Temperature),
Temperature: param.NewOpt(a.cfg.Temperature),
})
if err != nil {
return nil, fmt.Errorf("failed to parse vendor info: %w", err)

View File

@@ -30,6 +30,8 @@ type (
DocumentVersion struct {
ID gid.GID `db:"id"`
DocumentID gid.GID `db:"document_id"`
Title string `db:"title"`
OwnerID gid.GID `db:"owner_id"`
VersionNumber int `db:"version_number"`
Content string `db:"content"`
Changelog string `db:"changelog"`
@@ -55,6 +57,8 @@ func (p *DocumentVersions) LoadByDocumentID(
SELECT
id,
document_id,
title,
owner_id,
version_number,
content,
changelog,
@@ -71,10 +75,11 @@ WHERE
AND document_id = @document_id
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.StrictNamedArgs{"document_id": documentID}
args := pgx.StrictNamedArgs{
"document_id": documentID,
}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
@@ -112,6 +117,8 @@ func (p *DocumentVersion) LoadByID(
SELECT
id,
document_id,
title,
owner_id,
version_number,
content,
changelog,
@@ -131,7 +138,9 @@ LIMIT 1;
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"document_version_id": documentVersionID}
args := pgx.StrictNamedArgs{
"document_version_id": documentVersionID,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
@@ -159,6 +168,8 @@ INSERT INTO document_versions (
tenant_id,
id,
document_id,
title,
owner_id,
version_number,
content,
changelog,
@@ -166,10 +177,13 @@ INSERT INTO document_versions (
status,
created_at,
updated_at
) VALUES (
)
VALUES (
@tenant_id,
@id,
@document_id,
@title,
@owner_id,
@version_number,
@content,
@changelog,
@@ -179,24 +193,24 @@ INSERT INTO document_versions (
@updated_at
)
`
now := time.Now()
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"id": p.ID,
"document_id": p.DocumentID,
"title": p.Title,
"owner_id": p.OwnerID,
"version_number": p.VersionNumber,
"content": p.Content,
"changelog": p.Changelog,
"created_by": p.CreatedBy,
"status": p.Status,
"created_at": now,
"updated_at": now,
"created_at": p.CreatedAt,
"updated_at": p.UpdatedAt,
}
_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("error creating/updating document version: %w", err)
return fmt.Errorf("error creating document version: %w", err)
}
return nil
@@ -213,6 +227,8 @@ func (p *DocumentVersion) LoadByDocumentIDAndVersionNumber(
SELECT
id,
document_id,
title,
owner_id,
version_number,
content,
changelog,
@@ -237,7 +253,6 @@ LIMIT 1;
"document_id": documentID,
"version_number": versionNumber,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
@@ -265,6 +280,8 @@ func (p *DocumentVersion) LoadLatestVersion(
SELECT
id,
document_id,
title,
owner_id,
version_number,
content,
changelog,
@@ -282,13 +299,11 @@ WHERE
ORDER BY created_at DESC
LIMIT 1;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"document_id": documentID,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
@@ -313,6 +328,8 @@ func (p DocumentVersion) Update(
) error {
q := `
UPDATE document_versions SET
title = @title,
owner_id = @owner_id,
changelog = @changelog,
status = @status,
content = @content,
@@ -320,12 +337,15 @@ UPDATE document_versions SET
published_at = @published_at,
updated_at = @updated_at
WHERE %s
AND id = @document_version_id;`
AND id = @document_version_id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"document_version_id": p.ID,
"title": p.Title,
"owner_id": p.OwnerID,
"changelog": p.Changelog,
"status": p.Status,
"content": p.Content,

View File

@@ -0,0 +1,35 @@
ALTER INDEX policy_versions_pkey RENAME TO document_versions_pkey;
ALTER TABLE document_versions
RENAME CONSTRAINT policy_versions_policy_id_version_number_key
TO document_versions_document_id_version_number_key;
ALTER TABLE document_versions
RENAME CONSTRAINT policy_versions_published_by_fkey
TO document_versions_published_by_fkey;
ALTER TABLE document_versions
ADD CONSTRAINT document_versions_created_by_fkey
FOREIGN KEY (created_by) REFERENCES peoples(id)
ON DELETE RESTRICT
ON UPDATE CASCADE;
ALTER TABLE document_versions
ADD COLUMN title TEXT,
ADD COLUMN owner_id TEXT;
UPDATE document_versions dv
SET title = d.title,
owner_id = d.owner_id
FROM documents d
WHERE dv.document_id = d.id;
ALTER TABLE document_versions
ALTER COLUMN title SET NOT NULL,
ALTER COLUMN owner_id SET NOT NULL;
ALTER TABLE document_versions
ADD CONSTRAINT document_versions_owner_id_fkey
FOREIGN KEY (owner_id) REFERENCES peoples(id)
ON DELETE RESTRICT
ON UPDATE CASCADE;

View File

@@ -69,10 +69,58 @@ func (s *DocumentService) Get(
return document, nil
}
func (s DocumentService) GenerateChangelog(
ctx context.Context,
documentID gid.GID,
) (*string, error) {
draftVersion := &coredata.DocumentVersion{}
publishedVersion := &coredata.DocumentVersion{}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := draftVersion.LoadLatestVersion(ctx, conn, s.svc.scope, documentID); err != nil {
return fmt.Errorf("cannot load draft version: %w", err)
}
if draftVersion.Status != coredata.DocumentStatusDraft {
return fmt.Errorf("latest version is not a draft")
}
document := &coredata.Document{}
if err := document.LoadByID(ctx, conn, s.svc.scope, documentID); err != nil {
return fmt.Errorf("cannot load document: %w", err)
}
if document.CurrentPublishedVersion == nil {
publishedVersion.Content = ""
} else {
if err := publishedVersion.LoadByDocumentIDAndVersionNumber(ctx, conn, s.svc.scope, documentID, *document.CurrentPublishedVersion); err != nil {
return fmt.Errorf("cannot load published version: %w", err)
}
}
return nil
},
)
if err != nil {
return nil, err
}
changelog, err := s.svc.agent.GenerateChangelog(ctx, publishedVersion.Content, draftVersion.Content)
if err != nil {
return nil, fmt.Errorf("failed to generate changelog: %w", err)
}
return changelog, nil
}
func (s *DocumentService) PublishVersion(
ctx context.Context,
documentID gid.GID,
publishedBy gid.GID,
changelog *string,
) (*coredata.Document, *coredata.DocumentVersion, error) {
document := &coredata.Document{}
documentVersion := &coredata.DocumentVersion{}
@@ -93,6 +141,10 @@ func (s *DocumentService) PublishVersion(
return fmt.Errorf("cannot publish version")
}
if changelog != nil {
documentVersion.Changelog = *changelog
}
document.CurrentPublishedVersion = &documentVersion.VersionNumber
document.UpdatedAt = now
@@ -142,6 +194,8 @@ func (s *DocumentService) Create(
documentVersion := &coredata.DocumentVersion{
ID: documentVersionID,
DocumentID: documentID,
Title: req.Title,
OwnerID: req.OwnerID,
VersionNumber: 1,
Content: req.Content,
Status: coredata.DocumentStatusDraft,
@@ -149,6 +203,7 @@ func (s *DocumentService) Create(
CreatedAt: now,
UpdatedAt: now,
}
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
@@ -348,6 +403,7 @@ func (s *DocumentService) UpdateVersion(
req UpdateDocumentVersionRequest,
) (*coredata.DocumentVersion, error) {
documentVersion := &coredata.DocumentVersion{}
document := &coredata.Document{}
err := s.svc.pg.WithTx(
ctx,
@@ -356,10 +412,16 @@ func (s *DocumentService) UpdateVersion(
return fmt.Errorf("cannot load document version %q: %w", req.ID, err)
}
if err := document.LoadByID(ctx, conn, s.svc.scope, documentVersion.DocumentID); err != nil {
return fmt.Errorf("cannot load document %q: %w", documentVersion.DocumentID, err)
}
if documentVersion.Status != coredata.DocumentStatusDraft {
return fmt.Errorf("cannot update published version")
}
documentVersion.Title = document.Title
documentVersion.OwnerID = document.OwnerID
documentVersion.Content = req.Content
documentVersion.UpdatedAt = time.Now()
@@ -473,12 +535,17 @@ func (s *DocumentService) CreateDraft(
draftVersionID := gid.New(s.svc.scope.GetTenantID(), coredata.DocumentVersionEntityType)
latestVersion := &coredata.DocumentVersion{}
document := &coredata.Document{}
draftVersion := &coredata.DocumentVersion{}
now := time.Now()
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := document.LoadByID(ctx, conn, s.svc.scope, documentID); err != nil {
return fmt.Errorf("cannot load document: %w", err)
}
if err := latestVersion.LoadLatestVersion(ctx, conn, s.svc.scope, documentID); err != nil {
return fmt.Errorf("cannot load latest version: %w", err)
}
@@ -489,6 +556,8 @@ func (s *DocumentService) CreateDraft(
draftVersion.ID = draftVersionID
draftVersion.DocumentID = documentID
draftVersion.Title = document.Title
draftVersion.OwnerID = document.OwnerID
draftVersion.VersionNumber = latestVersion.VersionNumber + 1
draftVersion.Content = latestVersion.Content
draftVersion.Status = coredata.DocumentStatusDraft
@@ -640,6 +709,7 @@ func (s *DocumentService) Update(
documentID gid.GID,
newOwnerID *gid.GID,
documentType *coredata.DocumentType,
title *string,
) (*coredata.Document, error) {
document := &coredata.Document{}
people := &coredata.People{}
@@ -662,6 +732,11 @@ func (s *DocumentService) Update(
if documentType != nil {
document.DocumentType = *documentType
}
if title != nil {
document.Title = *title
}
document.UpdatedAt = now
if err := document.Update(ctx, tx, s.svc.scope); err != nil {

View File

@@ -29,13 +29,13 @@ import (
type (
Service struct {
pg *pg.Client
s3 *s3.Client
bucket string
encryptionKey cipher.EncryptionKey
hostname string
tokenSecret string
vendorAssessment agents.Config
pg *pg.Client
s3 *s3.Client
bucket string
encryptionKey cipher.EncryptionKey
hostname string
tokenSecret string
agentConfig agents.Config
}
TenantService struct {
@@ -46,7 +46,7 @@ type (
scope coredata.Scoper
hostname string
tokenSecret string
vendorAssessment *agents.VendorAssessment
agent *agents.Agent
Frameworks *FrameworkService
Measures *MeasureService
Tasks *TaskService
@@ -72,20 +72,20 @@ func NewService(
bucket string,
hostname string,
tokenSecret string,
vendorAssessment agents.Config,
agentConfig agents.Config,
) (*Service, error) {
if bucket == "" {
return nil, fmt.Errorf("bucket is required")
}
svc := &Service{
pg: pgClient,
s3: s3Client,
bucket: bucket,
encryptionKey: encryptionKey,
hostname: hostname,
tokenSecret: tokenSecret,
vendorAssessment: vendorAssessment,
pg: pgClient,
s3: s3Client,
bucket: bucket,
encryptionKey: encryptionKey,
hostname: hostname,
tokenSecret: tokenSecret,
agentConfig: agentConfig,
}
return svc, nil
@@ -93,14 +93,14 @@ func NewService(
func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
tenantService := &TenantService{
pg: s.pg,
s3: s.s3,
bucket: s.bucket,
encryptionKey: s.encryptionKey,
hostname: s.hostname,
scope: coredata.NewScope(tenantID),
tokenSecret: s.tokenSecret,
vendorAssessment: agents.NewVendorAssessment(nil, s.vendorAssessment),
pg: s.pg,
s3: s.s3,
bucket: s.bucket,
encryptionKey: s.encryptionKey,
hostname: s.hostname,
scope: coredata.NewScope(tenantID),
tokenSecret: s.tokenSecret,
agent: agents.NewAgent(nil, s.agentConfig),
}
tenantService.Frameworks = &FrameworkService{svc: tenantService}

View File

@@ -447,7 +447,7 @@ func (s VendorService) Assess(
ctx context.Context,
req AssessVendorRequest,
) (*coredata.Vendor, error) {
vendorInfo, err := s.svc.vendorAssessment.Fetch(ctx, req.WebsiteURL)
vendorInfo, err := s.svc.agent.AssessVendor(ctx, req.WebsiteURL)
if err != nil {
return nil, fmt.Errorf("failed to assess vendor info: %w", err)
}

View File

@@ -187,13 +187,13 @@ func (impl *Implm) Run(
}
}
vendorAssessmentConfig := agents.Config{
agentConfig := agents.Config{
OpenAIAPIKey: impl.cfg.OpenAI.APIKey,
Temperature: impl.cfg.OpenAI.Temperature,
ModelName: impl.cfg.OpenAI.ModelName,
}
vendorAssessment := agents.NewVendorAssessment(l.Named("vendor-assessment"), vendorAssessmentConfig)
agent := agents.NewAgent(l.Named("agent"), agentConfig)
usrmgrService, err := usrmgr.NewService(
ctx,
@@ -215,7 +215,7 @@ func (impl *Implm) Run(
impl.cfg.AWS.Bucket,
impl.cfg.Hostname,
impl.cfg.Auth.Cookie.Secret,
vendorAssessmentConfig,
agentConfig,
)
if err != nil {
return fmt.Errorf("cannot create probo service: %w", err)
@@ -227,7 +227,7 @@ func (impl *Implm) Run(
Probo: proboService,
Usrmgr: usrmgrService,
ConnectorRegistry: defaultConnectorRegistry,
VendorAssessment: vendorAssessment,
Agent: agent,
SafeRedirect: &saferedirect.SafeRedirect{AllowedHost: impl.cfg.Hostname},
Logger: l.Named("http.server"),
Auth: console_v1.AuthConfig{

View File

@@ -1157,6 +1157,9 @@ type Mutation {
publishDocumentVersion(
input: PublishDocumentVersionInput!
): PublishDocumentVersionPayload!
generateDocumentChangelog(
input: GenerateDocumentChangelogInput!
): GenerateDocumentChangelogPayload!
createDraftDocumentVersion(
input: CreateDraftDocumentVersionInput!
): CreateDraftDocumentVersionPayload!
@@ -1746,6 +1749,8 @@ type DocumentVersion implements Node {
version: Int!
content: String!
changelog: String!
title: String!
owner: People! @goField(forceResolver: true)
signatures(
first: Int
@@ -1827,6 +1832,7 @@ type RequestSignaturePayload {
input PublishDocumentVersionInput {
documentId: ID!
changelog: String
}
type PublishDocumentVersionPayload {
@@ -1885,6 +1891,14 @@ type ExportAuditPayload {
url: String!
}
input GenerateDocumentChangelogInput {
documentId: ID!
}
type GenerateDocumentChangelogPayload {
changelog: String!
}
input AssessVendorInput {
id: ID!
websiteUrl: String!

View File

@@ -343,10 +343,12 @@ type ComplexityRoot struct {
CreatedAt func(childComplexity int) int
Document func(childComplexity int) int
ID func(childComplexity int) int
Owner func(childComplexity int) int
PublishedAt func(childComplexity int) int
PublishedBy func(childComplexity int) int
Signatures func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentVersionSignatureOrder) int
Status func(childComplexity int) int
Title func(childComplexity int) int
UpdatedAt func(childComplexity int) int
Version func(childComplexity int) int
}
@@ -437,6 +439,10 @@ type ComplexityRoot struct {
EvidenceEdge func(childComplexity int) int
}
GenerateDocumentChangelogPayload struct {
Changelog func(childComplexity int) int
}
ImportFrameworkPayload struct {
FrameworkEdge func(childComplexity int) int
}
@@ -513,6 +519,7 @@ type ComplexityRoot struct {
DeleteVendorComplianceReport func(childComplexity int, input types.DeleteVendorComplianceReportInput) int
ExportAudit func(childComplexity int, input types.ExportAuditInput) int
FulfillEvidence func(childComplexity int, input types.FulfillEvidenceInput) int
GenerateDocumentChangelog func(childComplexity int, input types.GenerateDocumentChangelogInput) int
ImportFramework func(childComplexity int, input types.ImportFrameworkInput) int
ImportMeasure func(childComplexity int, input types.ImportMeasureInput) int
InviteUser func(childComplexity int, input types.InviteUserInput) int
@@ -889,6 +896,7 @@ type DocumentResolver interface {
type DocumentVersionResolver interface {
Document(ctx context.Context, obj *types.DocumentVersion) (*types.Document, error)
Owner(ctx context.Context, obj *types.DocumentVersion) (*types.People, error)
Signatures(ctx context.Context, obj *types.DocumentVersion, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentVersionSignatureOrder) (*types.DocumentVersionSignatureConnection, error)
PublishedBy(ctx context.Context, obj *types.DocumentVersion) (*types.People, error)
}
@@ -963,6 +971,7 @@ type MutationResolver interface {
UpdateDocument(ctx context.Context, input types.UpdateDocumentInput) (*types.UpdateDocumentPayload, error)
DeleteDocument(ctx context.Context, input types.DeleteDocumentInput) (*types.DeleteDocumentPayload, error)
PublishDocumentVersion(ctx context.Context, input types.PublishDocumentVersionInput) (*types.PublishDocumentVersionPayload, error)
GenerateDocumentChangelog(ctx context.Context, input types.GenerateDocumentChangelogInput) (*types.GenerateDocumentChangelogPayload, error)
CreateDraftDocumentVersion(ctx context.Context, input types.CreateDraftDocumentVersionInput) (*types.CreateDraftDocumentVersionPayload, error)
UpdateDocumentVersion(ctx context.Context, input types.UpdateDocumentVersionInput) (*types.UpdateDocumentVersionPayload, error)
RequestSignature(ctx context.Context, input types.RequestSignatureInput) (*types.RequestSignaturePayload, error)
@@ -1897,6 +1906,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.DocumentVersion.ID(childComplexity), true
case "DocumentVersion.owner":
if e.complexity.DocumentVersion.Owner == nil {
break
}
return e.complexity.DocumentVersion.Owner(childComplexity), true
case "DocumentVersion.publishedAt":
if e.complexity.DocumentVersion.PublishedAt == nil {
break
@@ -1930,6 +1946,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.DocumentVersion.Status(childComplexity), true
case "DocumentVersion.title":
if e.complexity.DocumentVersion.Title == nil {
break
}
return e.complexity.DocumentVersion.Title(childComplexity), true
case "DocumentVersion.updatedAt":
if e.complexity.DocumentVersion.UpdatedAt == nil {
break
@@ -2278,6 +2301,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.FulfillEvidencePayload.EvidenceEdge(childComplexity), true
case "GenerateDocumentChangelogPayload.changelog":
if e.complexity.GenerateDocumentChangelogPayload.Changelog == nil {
break
}
return e.complexity.GenerateDocumentChangelogPayload.Changelog(childComplexity), true
case "ImportFrameworkPayload.frameworkEdge":
if e.complexity.ImportFrameworkPayload.FrameworkEdge == nil {
break
@@ -2892,6 +2922,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Mutation.FulfillEvidence(childComplexity, args["input"].(types.FulfillEvidenceInput)), true
case "Mutation.generateDocumentChangelog":
if e.complexity.Mutation.GenerateDocumentChangelog == nil {
break
}
args, err := ec.field_Mutation_generateDocumentChangelog_args(ctx, rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.GenerateDocumentChangelog(childComplexity, args["input"].(types.GenerateDocumentChangelogInput)), true
case "Mutation.importFramework":
if e.complexity.Mutation.ImportFramework == nil {
break
@@ -4547,6 +4589,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputExportAuditInput,
ec.unmarshalInputFrameworkOrder,
ec.unmarshalInputFulfillEvidenceInput,
ec.unmarshalInputGenerateDocumentChangelogInput,
ec.unmarshalInputImportFrameworkInput,
ec.unmarshalInputImportMeasureInput,
ec.unmarshalInputInviteUserInput,
@@ -5837,6 +5880,9 @@ type Mutation {
publishDocumentVersion(
input: PublishDocumentVersionInput!
): PublishDocumentVersionPayload!
generateDocumentChangelog(
input: GenerateDocumentChangelogInput!
): GenerateDocumentChangelogPayload!
createDraftDocumentVersion(
input: CreateDraftDocumentVersionInput!
): CreateDraftDocumentVersionPayload!
@@ -6426,6 +6472,8 @@ type DocumentVersion implements Node {
version: Int!
content: String!
changelog: String!
title: String!
owner: People! @goField(forceResolver: true)
signatures(
first: Int
@@ -6507,6 +6555,7 @@ type RequestSignaturePayload {
input PublishDocumentVersionInput {
documentId: ID!
changelog: String
}
type PublishDocumentVersionPayload {
@@ -6565,6 +6614,14 @@ type ExportAuditPayload {
url: String!
}
input GenerateDocumentChangelogInput {
documentId: ID!
}
type GenerateDocumentChangelogPayload {
changelog: String!
}
input AssessVendorInput {
id: ID!
websiteUrl: String!
@@ -8799,6 +8856,29 @@ func (ec *executionContext) field_Mutation_fulfillEvidence_argsInput(
return zeroVal, nil
}
func (ec *executionContext) field_Mutation_generateDocumentChangelog_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
arg0, err := ec.field_Mutation_generateDocumentChangelog_argsInput(ctx, rawArgs)
if err != nil {
return nil, err
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_generateDocumentChangelog_argsInput(
ctx context.Context,
rawArgs map[string]any,
) (types.GenerateDocumentChangelogInput, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
if tmp, ok := rawArgs["input"]; ok {
return ec.unmarshalNGenerateDocumentChangelogInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐGenerateDocumentChangelogInput(ctx, tmp)
}
var zeroVal types.GenerateDocumentChangelogInput
return zeroVal, nil
}
func (ec *executionContext) field_Mutation_importFramework_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
@@ -17134,6 +17214,116 @@ func (ec *executionContext) fieldContext_DocumentVersion_changelog(_ context.Con
return fc, nil
}
func (ec *executionContext) _DocumentVersion_title(ctx context.Context, field graphql.CollectedField, obj *types.DocumentVersion) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_DocumentVersion_title(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.Title, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_DocumentVersion_title(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "DocumentVersion",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _DocumentVersion_owner(ctx context.Context, field graphql.CollectedField, obj *types.DocumentVersion) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_DocumentVersion_owner(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.DocumentVersion().Owner(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*types.People)
fc.Result = res
return ec.marshalNPeople2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐPeople(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_DocumentVersion_owner(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "DocumentVersion",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "id":
return ec.fieldContext_People_id(ctx, field)
case "fullName":
return ec.fieldContext_People_fullName(ctx, field)
case "primaryEmailAddress":
return ec.fieldContext_People_primaryEmailAddress(ctx, field)
case "additionalEmailAddresses":
return ec.fieldContext_People_additionalEmailAddresses(ctx, field)
case "kind":
return ec.fieldContext_People_kind(ctx, field)
case "position":
return ec.fieldContext_People_position(ctx, field)
case "contractStartDate":
return ec.fieldContext_People_contractStartDate(ctx, field)
case "contractEndDate":
return ec.fieldContext_People_contractEndDate(ctx, field)
case "createdAt":
return ec.fieldContext_People_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_People_updatedAt(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type People", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _DocumentVersion_signatures(ctx context.Context, field graphql.CollectedField, obj *types.DocumentVersion) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_DocumentVersion_signatures(ctx, field)
if err != nil {
@@ -17586,6 +17776,10 @@ func (ec *executionContext) fieldContext_DocumentVersionEdge_node(_ context.Cont
return ec.fieldContext_DocumentVersion_content(ctx, field)
case "changelog":
return ec.fieldContext_DocumentVersion_changelog(ctx, field)
case "title":
return ec.fieldContext_DocumentVersion_title(ctx, field)
case "owner":
return ec.fieldContext_DocumentVersion_owner(ctx, field)
case "signatures":
return ec.fieldContext_DocumentVersion_signatures(ctx, field)
case "publishedBy":
@@ -17698,6 +17892,10 @@ func (ec *executionContext) fieldContext_DocumentVersionSignature_documentVersio
return ec.fieldContext_DocumentVersion_content(ctx, field)
case "changelog":
return ec.fieldContext_DocumentVersion_changelog(ctx, field)
case "title":
return ec.fieldContext_DocumentVersion_title(ctx, field)
case "owner":
return ec.fieldContext_DocumentVersion_owner(ctx, field)
case "signatures":
return ec.fieldContext_DocumentVersion_signatures(ctx, field)
case "publishedBy":
@@ -19768,6 +19966,50 @@ func (ec *executionContext) fieldContext_FulfillEvidencePayload_evidenceEdge(_ c
return fc, nil
}
func (ec *executionContext) _GenerateDocumentChangelogPayload_changelog(ctx context.Context, field graphql.CollectedField, obj *types.GenerateDocumentChangelogPayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_GenerateDocumentChangelogPayload_changelog(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.Changelog, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_GenerateDocumentChangelogPayload_changelog(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "GenerateDocumentChangelogPayload",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _ImportFrameworkPayload_frameworkEdge(ctx context.Context, field graphql.CollectedField, obj *types.ImportFrameworkPayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_ImportFrameworkPayload_frameworkEdge(ctx, field)
if err != nil {
@@ -23473,6 +23715,65 @@ func (ec *executionContext) fieldContext_Mutation_publishDocumentVersion(ctx con
return fc, nil
}
func (ec *executionContext) _Mutation_generateDocumentChangelog(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_generateDocumentChangelog(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().GenerateDocumentChangelog(rctx, fc.Args["input"].(types.GenerateDocumentChangelogInput))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*types.GenerateDocumentChangelogPayload)
fc.Result = res
return ec.marshalNGenerateDocumentChangelogPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐGenerateDocumentChangelogPayload(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Mutation_generateDocumentChangelog(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Mutation",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "changelog":
return ec.fieldContext_GenerateDocumentChangelogPayload_changelog(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type GenerateDocumentChangelogPayload", field.Name)
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_generateDocumentChangelog_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
return fc, nil
}
func (ec *executionContext) _Mutation_createDraftDocumentVersion(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_createDraftDocumentVersion(ctx, field)
if err != nil {
@@ -26456,6 +26757,10 @@ func (ec *executionContext) fieldContext_PublishDocumentVersionPayload_documentV
return ec.fieldContext_DocumentVersion_content(ctx, field)
case "changelog":
return ec.fieldContext_DocumentVersion_changelog(ctx, field)
case "title":
return ec.fieldContext_DocumentVersion_title(ctx, field)
case "owner":
return ec.fieldContext_DocumentVersion_owner(ctx, field)
case "signatures":
return ec.fieldContext_DocumentVersion_signatures(ctx, field)
case "publishedBy":
@@ -29462,6 +29767,10 @@ func (ec *executionContext) fieldContext_UpdateDocumentVersionPayload_documentVe
return ec.fieldContext_DocumentVersion_content(ctx, field)
case "changelog":
return ec.fieldContext_DocumentVersion_changelog(ctx, field)
case "title":
return ec.fieldContext_DocumentVersion_title(ctx, field)
case "owner":
return ec.fieldContext_DocumentVersion_owner(ctx, field)
case "signatures":
return ec.fieldContext_DocumentVersion_signatures(ctx, field)
case "publishedBy":
@@ -37516,6 +37825,33 @@ func (ec *executionContext) unmarshalInputFulfillEvidenceInput(ctx context.Conte
return it, nil
}
func (ec *executionContext) unmarshalInputGenerateDocumentChangelogInput(ctx context.Context, obj any) (types.GenerateDocumentChangelogInput, error) {
var it types.GenerateDocumentChangelogInput
asMap := map[string]any{}
for k, v := range obj.(map[string]any) {
asMap[k] = v
}
fieldsInOrder := [...]string{"documentId"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
case "documentId":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("documentId"))
data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v)
if err != nil {
return it, err
}
it.DocumentID = data
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputImportFrameworkInput(ctx context.Context, obj any) (types.ImportFrameworkInput, error) {
var it types.ImportFrameworkInput
asMap := map[string]any{}
@@ -37734,7 +38070,7 @@ func (ec *executionContext) unmarshalInputPublishDocumentVersionInput(ctx contex
asMap[k] = v
}
fieldsInOrder := [...]string{"documentId"}
fieldsInOrder := [...]string{"documentId", "changelog"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
@@ -37748,6 +38084,13 @@ func (ec *executionContext) unmarshalInputPublishDocumentVersionInput(ctx contex
return it, err
}
it.DocumentID = data
case "changelog":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("changelog"))
data, err := ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
it.Changelog = data
}
}
@@ -42067,6 +42410,47 @@ func (ec *executionContext) _DocumentVersion(ctx context.Context, sel ast.Select
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "title":
out.Values[i] = ec._DocumentVersion_title(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "owner":
field := field
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._DocumentVersion_owner(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&fs.Invalids, 1)
}
return res
}
if field.Deferrable != nil {
dfs, ok := deferred[field.Deferrable.Label]
di := 0
if ok {
dfs.AddField(field)
di = len(dfs.Values) - 1
} else {
dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
deferred[field.Deferrable.Label] = dfs
}
dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
return innerFunc(ctx, dfs)
})
// don't run the out.Concurrently() call below
out.Values[i] = graphql.Null
continue
}
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
case "signatures":
field := field
@@ -43084,6 +43468,45 @@ func (ec *executionContext) _FulfillEvidencePayload(ctx context.Context, sel ast
return out
}
var generateDocumentChangelogPayloadImplementors = []string{"GenerateDocumentChangelogPayload"}
func (ec *executionContext) _GenerateDocumentChangelogPayload(ctx context.Context, sel ast.SelectionSet, obj *types.GenerateDocumentChangelogPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, generateDocumentChangelogPayloadImplementors)
out := graphql.NewFieldSet(fields)
deferred := make(map[string]*graphql.FieldSet)
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("GenerateDocumentChangelogPayload")
case "changelog":
out.Values[i] = ec._GenerateDocumentChangelogPayload_changelog(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch(ctx)
if out.Invalids > 0 {
return graphql.Null
}
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
for label, dfs := range deferred {
ec.processDeferredGroup(graphql.DeferredGroup{
Label: label,
Path: graphql.GetPath(ctx),
FieldSet: dfs,
Context: ctx,
})
}
return out
}
var importFrameworkPayloadImplementors = []string{"ImportFrameworkPayload"}
func (ec *executionContext) _ImportFrameworkPayload(ctx context.Context, sel ast.SelectionSet, obj *types.ImportFrameworkPayload) graphql.Marshaler {
@@ -43850,6 +44273,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "generateDocumentChangelog":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_generateDocumentChangelog(ctx, field)
})
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "createDraftDocumentVersion":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_createDraftDocumentVersion(ctx, field)
@@ -49850,6 +50280,25 @@ func (ec *executionContext) marshalNFulfillEvidencePayload2ᚖgithubᚗcomᚋget
return ec._FulfillEvidencePayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNGenerateDocumentChangelogInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐGenerateDocumentChangelogInput(ctx context.Context, v any) (types.GenerateDocumentChangelogInput, error) {
res, err := ec.unmarshalInputGenerateDocumentChangelogInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNGenerateDocumentChangelogPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐGenerateDocumentChangelogPayload(ctx context.Context, sel ast.SelectionSet, v types.GenerateDocumentChangelogPayload) graphql.Marshaler {
return ec._GenerateDocumentChangelogPayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNGenerateDocumentChangelogPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐGenerateDocumentChangelogPayload(ctx context.Context, sel ast.SelectionSet, v *types.GenerateDocumentChangelogPayload) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
}
return graphql.Null
}
return ec._GenerateDocumentChangelogPayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx context.Context, v any) (gid.GID, error) {
res, err := types.UnmarshalGIDScalar(v)
return res, graphql.ErrorOnPath(ctx, err)

View File

@@ -46,6 +46,7 @@ func NewDocumentVersion(documentVersion *coredata.DocumentVersion) *DocumentVers
return &DocumentVersion{
ID: documentVersion.ID,
Version: documentVersion.VersionNumber,
Title: documentVersion.Title,
Content: documentVersion.Content,
Status: documentVersion.Status,
PublishedAt: documentVersion.PublishedAt,

View File

@@ -557,6 +557,8 @@ type DocumentVersion struct {
Version int `json:"version"`
Content string `json:"content"`
Changelog string `json:"changelog"`
Title string `json:"title"`
Owner *People `json:"owner"`
Signatures *DocumentVersionSignatureConnection `json:"signatures"`
PublishedBy *People `json:"publishedBy,omitempty"`
PublishedAt *time.Time `json:"publishedAt,omitempty"`
@@ -682,6 +684,14 @@ type FulfillEvidencePayload struct {
EvidenceEdge *EvidenceEdge `json:"evidenceEdge"`
}
type GenerateDocumentChangelogInput struct {
DocumentID gid.GID `json:"documentId"`
}
type GenerateDocumentChangelogPayload struct {
Changelog string `json:"changelog"`
}
type ImportFrameworkInput struct {
OrganizationID gid.GID `json:"organizationId"`
File graphql.Upload `json:"file"`
@@ -812,6 +822,7 @@ type PeopleEdge struct {
type PublishDocumentVersionInput struct {
DocumentID gid.GID `json:"documentId"`
Changelog *string `json:"changelog,omitempty"`
}
type PublishDocumentVersionPayload struct {

View File

@@ -302,6 +302,23 @@ func (r *documentVersionResolver) Document(ctx context.Context, obj *types.Docum
return types.NewDocument(document), nil
}
// Owner is the resolver for the owner field.
func (r *documentVersionResolver) Owner(ctx context.Context, obj *types.DocumentVersion) (*types.People, error) {
svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID())
documentVersion, err := svc.Documents.GetVersion(ctx, obj.ID)
if err != nil {
panic(fmt.Errorf("cannot get document version: %w", err))
}
owner, err := svc.Peoples.Get(ctx, documentVersion.OwnerID)
if err != nil {
panic(fmt.Errorf("cannot get owner: %w", err))
}
return types.NewPeople(owner), nil
}
// Signatures is the resolver for the signatures field.
func (r *documentVersionResolver) Signatures(ctx context.Context, obj *types.DocumentVersion, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentVersionSignatureOrder) (*types.DocumentVersionSignatureConnection, error) {
svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID())
@@ -1491,6 +1508,7 @@ func (r *mutationResolver) UpdateDocument(ctx context.Context, input types.Updat
input.ID,
input.OwnerID,
input.DocumentType,
input.Title,
)
if err != nil {
@@ -1526,7 +1544,7 @@ func (r *mutationResolver) PublishDocumentVersion(ctx context.Context, input typ
panic(fmt.Errorf("cannot get people: %w", err))
}
document, documentVersion, err := svc.Documents.PublishVersion(ctx, input.DocumentID, people.ID)
document, documentVersion, err := svc.Documents.PublishVersion(ctx, input.DocumentID, people.ID, input.Changelog)
if err != nil {
panic(fmt.Errorf("cannot publish document version: %w", err))
}
@@ -1537,6 +1555,20 @@ func (r *mutationResolver) PublishDocumentVersion(ctx context.Context, input typ
}, nil
}
// GenerateDocumentChangelog is the resolver for the generateDocumentChangelog field.
func (r *mutationResolver) GenerateDocumentChangelog(ctx context.Context, input types.GenerateDocumentChangelogInput) (*types.GenerateDocumentChangelogPayload, error) {
svc := GetTenantService(ctx, r.proboSvc, input.DocumentID.TenantID())
changelog, err := svc.Documents.GenerateChangelog(ctx, input.DocumentID)
if err != nil {
panic(fmt.Errorf("cannot generate document changelog: %w", err))
}
return &types.GenerateDocumentChangelogPayload{
Changelog: *changelog,
}, nil
}
// CreateDraftDocumentVersion is the resolver for the createDraftDocumentVersion field.
func (r *mutationResolver) CreateDraftDocumentVersion(ctx context.Context, input types.CreateDraftDocumentVersionInput) (*types.CreateDraftDocumentVersionPayload, error) {
svc := GetTenantService(ctx, r.proboSvc, input.DocumentID.TenantID())

View File

@@ -38,7 +38,7 @@ type Config struct {
Usrmgr *usrmgr.Service
Auth console_v1.AuthConfig
ConnectorRegistry *connector.ConnectorRegistry
VendorAssessment *agents.VendorAssessment
Agent *agents.Agent
SafeRedirect *saferedirect.SafeRedirect
Logger *log.Logger
}