@@ -4,6 +4,7 @@ import { useMutationWithToasts } from "../useMutationWithToasts";
|
|||||||
import type { DocumentGraphDeleteMutation } from "./__generated__/DocumentGraphDeleteMutation.graphql";
|
import type { DocumentGraphDeleteMutation } from "./__generated__/DocumentGraphDeleteMutation.graphql";
|
||||||
import type { DocumentGraphSendSigningNotificationsMutation } from "./__generated__/DocumentGraphSendSigningNotificationsMutation.graphql";
|
import type { DocumentGraphSendSigningNotificationsMutation } from "./__generated__/DocumentGraphSendSigningNotificationsMutation.graphql";
|
||||||
import type { DocumentGraphDeleteDraftMutation } from "./__generated__/DocumentGraphDeleteDraftMutation.graphql";
|
import type { DocumentGraphDeleteDraftMutation } from "./__generated__/DocumentGraphDeleteDraftMutation.graphql";
|
||||||
|
import type { DocumentGraphBulkExportDocumentsMutation } from "./__generated__/DocumentGraphBulkExportDocumentsMutation.graphql";
|
||||||
|
|
||||||
export const documentsQuery = graphql`
|
export const documentsQuery = graphql`
|
||||||
query DocumentGraphListQuery($organizationId: ID!) {
|
query DocumentGraphListQuery($organizationId: ID!) {
|
||||||
@@ -61,6 +62,28 @@ export function useDeleteDraftDocumentVersionMutation() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bulkDeleteDocumentsMutation = graphql`
|
||||||
|
mutation DocumentGraphBulkDeleteDocumentsMutation(
|
||||||
|
$input: BulkDeleteDocumentsInput!
|
||||||
|
) {
|
||||||
|
bulkDeleteDocuments(input: $input) {
|
||||||
|
deletedDocumentIds @deleteRecord
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function useBulkDeleteDocumentsMutation() {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
|
return useMutationWithToasts(
|
||||||
|
bulkDeleteDocumentsMutation,
|
||||||
|
{
|
||||||
|
successMessage: __("Documents deleted successfully."),
|
||||||
|
errorMessage: __("Failed to delete documents. Please try again."),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const sendSigningNotificationsMutation = graphql`
|
const sendSigningNotificationsMutation = graphql`
|
||||||
mutation DocumentGraphSendSigningNotificationsMutation(
|
mutation DocumentGraphSendSigningNotificationsMutation(
|
||||||
$input: SendSigningNotificationsInput!
|
$input: SendSigningNotificationsInput!
|
||||||
@@ -83,6 +106,28 @@ export function useSendSigningNotificationsMutation() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bulkExportDocumentsMutation = graphql`
|
||||||
|
mutation DocumentGraphBulkExportDocumentsMutation(
|
||||||
|
$input: BulkExportDocumentsInput!
|
||||||
|
) {
|
||||||
|
bulkExportDocuments(input: $input) {
|
||||||
|
exportJobId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function useBulkExportDocumentsMutation() {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
|
return useMutationWithToasts<DocumentGraphBulkExportDocumentsMutation>(
|
||||||
|
bulkExportDocumentsMutation,
|
||||||
|
{
|
||||||
|
successMessage: __("Document export started successfully. You will receive an email when the export is ready."),
|
||||||
|
errorMessage: __("Failed to start document export. Please try again."),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const documentNodeQuery = graphql`
|
export const documentNodeQuery = graphql`
|
||||||
query DocumentGraphNodeQuery($documentId: ID!) {
|
query DocumentGraphNodeQuery($documentId: ID!) {
|
||||||
node(id: $documentId) {
|
node(id: $documentId) {
|
||||||
|
|||||||
115
apps/console/src/hooks/graph/__generated__/DocumentGraphBulkDeleteDocumentsMutation.graphql.ts
generated
Normal file
115
apps/console/src/hooks/graph/__generated__/DocumentGraphBulkDeleteDocumentsMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<a26f31885b4f92b2ad9398bce7f538cf>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type BulkDeleteDocumentsInput = {
|
||||||
|
documentIds: ReadonlyArray<string>;
|
||||||
|
};
|
||||||
|
export type DocumentGraphBulkDeleteDocumentsMutation$variables = {
|
||||||
|
input: BulkDeleteDocumentsInput;
|
||||||
|
};
|
||||||
|
export type DocumentGraphBulkDeleteDocumentsMutation$data = {
|
||||||
|
readonly bulkDeleteDocuments: {
|
||||||
|
readonly deletedDocumentIds: ReadonlyArray<string>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type DocumentGraphBulkDeleteDocumentsMutation = {
|
||||||
|
response: DocumentGraphBulkDeleteDocumentsMutation$data;
|
||||||
|
variables: DocumentGraphBulkDeleteDocumentsMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v2 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "deletedDocumentIds",
|
||||||
|
"storageKey": null
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "DocumentGraphBulkDeleteDocumentsMutation",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": "BulkDeleteDocumentsPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "bulkDeleteDocuments",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "DocumentGraphBulkDeleteDocumentsMutation",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": "BulkDeleteDocumentsPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "bulkDeleteDocuments",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"filters": null,
|
||||||
|
"handle": "deleteRecord",
|
||||||
|
"key": "",
|
||||||
|
"kind": "ScalarHandle",
|
||||||
|
"name": "deletedDocumentIds"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "655f43d282b8e0007f3b93e381e7ff84",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "DocumentGraphBulkDeleteDocumentsMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation DocumentGraphBulkDeleteDocumentsMutation(\n $input: BulkDeleteDocumentsInput!\n) {\n bulkDeleteDocuments(input: $input) {\n deletedDocumentIds\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "919857a24f366b8ebe004fcadf503c3d";
|
||||||
|
|
||||||
|
export default node;
|
||||||
92
apps/console/src/hooks/graph/__generated__/DocumentGraphBulkExportDocumentsMutation.graphql.ts
generated
Normal file
92
apps/console/src/hooks/graph/__generated__/DocumentGraphBulkExportDocumentsMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<a398c636bba3d35aa7caef66c62c9ac6>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type BulkExportDocumentsInput = {
|
||||||
|
documentIds: ReadonlyArray<string>;
|
||||||
|
};
|
||||||
|
export type DocumentGraphBulkExportDocumentsMutation$variables = {
|
||||||
|
input: BulkExportDocumentsInput;
|
||||||
|
};
|
||||||
|
export type DocumentGraphBulkExportDocumentsMutation$data = {
|
||||||
|
readonly bulkExportDocuments: {
|
||||||
|
readonly exportJobId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type DocumentGraphBulkExportDocumentsMutation = {
|
||||||
|
response: DocumentGraphBulkExportDocumentsMutation$data;
|
||||||
|
variables: DocumentGraphBulkExportDocumentsMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "BulkExportDocumentsPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "bulkExportDocuments",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "exportJobId",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "DocumentGraphBulkExportDocumentsMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "DocumentGraphBulkExportDocumentsMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "2f4eb206613f8fba307aeee5bad27b93",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "DocumentGraphBulkExportDocumentsMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation DocumentGraphBulkExportDocumentsMutation(\n $input: BulkExportDocumentsInput!\n) {\n bulkExportDocuments(input: $input) {\n exportJobId\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "0a7bae4b58713bcc824a774c31bd6dca";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -524,12 +524,14 @@ export default function DocumentDetailPage(props: Props) {
|
|||||||
{__("Controls")}
|
{__("Controls")}
|
||||||
<TabBadge>{controlsCount}</TabBadge>
|
<TabBadge>{controlsCount}</TabBadge>
|
||||||
</TabLink>
|
</TabLink>
|
||||||
|
{!isDraft && (
|
||||||
<TabLink to={`${urlPrefix}/signatures`}>
|
<TabLink to={`${urlPrefix}/signatures`}>
|
||||||
{__("Signatures")}
|
{__("Signatures")}
|
||||||
<TabBadge>
|
<TabBadge>
|
||||||
{signedSignatures.length}/{signatures.length}
|
{signedSignatures.length}/{signatures.length}
|
||||||
</TabBadge>
|
</TabBadge>
|
||||||
</TabLink>
|
</TabLink>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Outlet context={{ document, version: currentVersion }} />
|
<Outlet context={{ document, version: currentVersion }} />
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import {
|
|||||||
IconCrossLargeX,
|
IconCrossLargeX,
|
||||||
IconSignature,
|
IconSignature,
|
||||||
IconCheckmark1,
|
IconCheckmark1,
|
||||||
|
IconArrowDown,
|
||||||
|
Card,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import {
|
import {
|
||||||
useFragment,
|
useFragment,
|
||||||
@@ -32,6 +34,8 @@ import {
|
|||||||
documentsQuery,
|
documentsQuery,
|
||||||
useDeleteDocumentMutation,
|
useDeleteDocumentMutation,
|
||||||
useSendSigningNotificationsMutation,
|
useSendSigningNotificationsMutation,
|
||||||
|
useBulkDeleteDocumentsMutation,
|
||||||
|
useBulkExportDocumentsMutation,
|
||||||
} from "/hooks/graph/DocumentGraph";
|
} from "/hooks/graph/DocumentGraph";
|
||||||
import type { DocumentsPageListFragment$key } from "./__generated__/DocumentsPageListFragment.graphql";
|
import type { DocumentsPageListFragment$key } from "./__generated__/DocumentsPageListFragment.graphql";
|
||||||
import { useList, usePageTitle } from "@probo/hooks";
|
import { useList, usePageTitle } from "@probo/hooks";
|
||||||
@@ -94,7 +98,10 @@ export default function DocumentsPage(props: Props) {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
const connectionId = pagination.data.documents.__id;
|
const connectionId = pagination.data.documents.__id;
|
||||||
const [sendSigningNotifications] = useSendSigningNotificationsMutation();
|
const [sendSigningNotifications] = useSendSigningNotificationsMutation();
|
||||||
|
const [bulkDeleteDocuments] = useBulkDeleteDocumentsMutation();
|
||||||
|
const [bulkExportDocuments] = useBulkExportDocumentsMutation();
|
||||||
const { list: selection, toggle, clear, reset } = useList<string>([]);
|
const { list: selection, toggle, clear, reset } = useList<string>([]);
|
||||||
|
const confirm = useConfirm();
|
||||||
|
|
||||||
usePageTitle(__("Documents"));
|
usePageTitle(__("Documents"));
|
||||||
|
|
||||||
@@ -106,6 +113,39 @@ export default function DocumentsPage(props: Props) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBulkDelete = () => {
|
||||||
|
const documentCount = selection.length;
|
||||||
|
confirm(
|
||||||
|
() =>
|
||||||
|
bulkDeleteDocuments({
|
||||||
|
variables: {
|
||||||
|
input: { documentIds: selection },
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
clear();
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
message: sprintf(
|
||||||
|
__(
|
||||||
|
'This will permanently delete %s document%s. This action cannot be undone.'
|
||||||
|
),
|
||||||
|
documentCount,
|
||||||
|
documentCount > 1 ? 's' : ''
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkExport = () => {
|
||||||
|
bulkExportDocuments({
|
||||||
|
variables: {
|
||||||
|
input: { documentIds: selection },
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
clear();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -126,28 +166,29 @@ export default function DocumentsPage(props: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
{documents.length > 0 ? (
|
||||||
<SortableTable {...pagination}>
|
<SortableTable {...pagination}>
|
||||||
<Thead>
|
<Thead>
|
||||||
{selection.length === 0 ? (
|
{selection.length === 0 ? (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>
|
<Th className="w-18">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selection.length === documents.length && documents.length > 0}
|
checked={selection.length === documents.length && documents.length > 0}
|
||||||
onChange={() => reset(documents.map((d) => d.id))}
|
onChange={() => reset(documents.map((d) => d.id))}
|
||||||
/>
|
/>
|
||||||
</Th>
|
</Th>
|
||||||
<SortableTh field="TITLE">{__("Name")}</SortableTh>
|
<SortableTh field="TITLE" className="min-w-0">{__("Name")}</SortableTh>
|
||||||
<Th>{__("Status")}</Th>
|
<Th className="w-24">{__("Status")}</Th>
|
||||||
<SortableTh field="DOCUMENT_TYPE">{__("Type")}</SortableTh>
|
<SortableTh field="DOCUMENT_TYPE" className="w-28">{__("Type")}</SortableTh>
|
||||||
<Th>{__("Owner")}</Th>
|
<Th className="w-60">{__("Owner")}</Th>
|
||||||
<Th>{__("Last update")}</Th>
|
<Th className="w-60">{__("Last update")}</Th>
|
||||||
<Th>{__("Signatures")}</Th>
|
<Th className="w-20">{__("Signatures")}</Th>
|
||||||
<Th></Th>
|
<Th className="w-18"></Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
) : (
|
) : (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th colspan={8}>
|
<Th colspan={8} compact>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center h-8">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
{sprintf(__("%s documents selected"), selection.length)} -
|
{sprintf(__("%s documents selected"), selection.length)} -
|
||||||
<button
|
<button
|
||||||
@@ -158,21 +199,39 @@ export default function DocumentsPage(props: Props) {
|
|||||||
{__("Clear selection")}
|
{__("Clear selection")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 items-center">
|
||||||
<PublishDocumentsDialog
|
<PublishDocumentsDialog
|
||||||
documentIds={selection}
|
documentIds={selection}
|
||||||
onSave={clear}
|
onSave={clear}
|
||||||
>
|
>
|
||||||
<Button icon={IconCheckmark1}>{__("Publish")}</Button>
|
<Button icon={IconCheckmark1} className="py-0.5 px-2 text-xs h-6 min-h-6">
|
||||||
|
{__("Publish")}
|
||||||
|
</Button>
|
||||||
</PublishDocumentsDialog>
|
</PublishDocumentsDialog>
|
||||||
<SignatureDocumentsDialog
|
<SignatureDocumentsDialog
|
||||||
documentIds={selection}
|
documentIds={selection}
|
||||||
onSave={clear}
|
onSave={clear}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" icon={IconSignature}>
|
<Button variant="secondary" icon={IconSignature} className="py-0.5 px-2 text-xs h-6 min-h-6">
|
||||||
{__("Request signature")}
|
{__("Request signature")}
|
||||||
</Button>
|
</Button>
|
||||||
</SignatureDocumentsDialog>
|
</SignatureDocumentsDialog>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
icon={IconArrowDown}
|
||||||
|
onClick={handleBulkExport}
|
||||||
|
className="py-0.5 px-2 text-xs h-6 min-h-6"
|
||||||
|
>
|
||||||
|
{__("Export")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
icon={IconTrashCan}
|
||||||
|
onClick={handleBulkDelete}
|
||||||
|
className="py-0.5 px-2 text-xs h-6 min-h-6"
|
||||||
|
>
|
||||||
|
{__("Delete")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Th>
|
</Th>
|
||||||
@@ -192,6 +251,18 @@ export default function DocumentsPage(props: Props) {
|
|||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</SortableTable>
|
</SortableTable>
|
||||||
|
) : (
|
||||||
|
<Card padded>
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<h3 className="text-lg font-semibold mb-2">
|
||||||
|
{__("No documents yet")}
|
||||||
|
</h3>
|
||||||
|
<p className="text-txt-tertiary mb-4">
|
||||||
|
{__("Create your first document to get started.")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -278,34 +349,27 @@ function DocumentRow({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr to={`/organizations/${organizationId}/documents/${document.id}`}>
|
<Tr to={`/organizations/${organizationId}/documents/${document.id}`}>
|
||||||
<Td noLink>
|
<Td noLink className="w-18">
|
||||||
<Checkbox checked={checked} onChange={onCheck} />
|
<Checkbox checked={checked} onChange={onCheck} />
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td className="min-w-0">
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
<img
|
|
||||||
src="/document.png"
|
|
||||||
alt=""
|
|
||||||
width={28}
|
|
||||||
height={36}
|
|
||||||
className="border-4 border-highlight rounded box-content"
|
|
||||||
/>
|
|
||||||
{document.title}
|
{document.title}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td className="w-24">
|
||||||
<Badge variant={isDraft ? "neutral" : "success"}>
|
<Badge variant={isDraft ? "neutral" : "success"}>
|
||||||
{isDraft ? __("Draft") : __("Published")}
|
{isDraft ? __("Draft") : __("Published")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{getDocumentTypeLabel(__, document.documentType)}</Td>
|
<Td className="w-28">{getDocumentTypeLabel(__, document.documentType)}</Td>
|
||||||
<Td>
|
<Td className="w-60">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<Avatar name={document.owner?.fullName ?? ""} />
|
<Avatar name={document.owner?.fullName ?? ""} />
|
||||||
{document.owner?.fullName}
|
{document.owner?.fullName}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td className="w-60">
|
||||||
{dateFormat(document.updatedAt, {
|
{dateFormat(document.updatedAt, {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
@@ -313,10 +377,10 @@ function DocumentRow({
|
|||||||
weekday: "short",
|
weekday: "short",
|
||||||
})}
|
})}
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td className="w-20">
|
||||||
{signedCount}/{signatures.length}
|
{signedCount}/{signatures.length}
|
||||||
</Td>
|
</Td>
|
||||||
<Td noLink width={50} className="text-end">
|
<Td noLink width={50} className="text-end w-18">
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export default function FrameworkDetailPage(props: Props) {
|
|||||||
exportFrameworkMutation,
|
exportFrameworkMutation,
|
||||||
{
|
{
|
||||||
errorMessage: "Failed to export framework",
|
errorMessage: "Failed to export framework",
|
||||||
successMessage: "Framework export started successfully",
|
successMessage: "Framework export started successfully. You will receive an email when the export is ready.",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -35,17 +35,20 @@ export function Th({
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
width,
|
width,
|
||||||
|
compact,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
colspan?: number;
|
colspan?: number;
|
||||||
|
compact?: boolean;
|
||||||
} & ThHTMLAttributes<HTMLTableCellElement>) {
|
} & ThHTMLAttributes<HTMLTableCellElement>) {
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
{...props}
|
{...props}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"first:pl-6 last:pr-6 py-3 whitespace-nowrap",
|
"first:pl-6 last:pr-6 whitespace-nowrap",
|
||||||
|
compact ? "py-1" : "py-3",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
style={{ width }}
|
style={{ width }}
|
||||||
|
|||||||
@@ -546,3 +546,27 @@ WHERE
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Documents) BulkSoftDelete(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
UPDATE documents SET deleted_at = @deleted_at WHERE %s AND id = ANY(@document_ids)
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
ids := make([]gid.GID, len(*p))
|
||||||
|
for i, doc := range *p {
|
||||||
|
ids[i] = doc.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"document_ids": ids,
|
||||||
|
"deleted_at": time.Now()}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
_, err := conn.Exec(ctx, q, args)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,5 +55,5 @@ const (
|
|||||||
SnapshotEntityType
|
SnapshotEntityType
|
||||||
ContinualImprovementEntityType
|
ContinualImprovementEntityType
|
||||||
ProcessingActivityEntityType
|
ProcessingActivityEntityType
|
||||||
FrameworkExportEntityType
|
ExportJobEntityType
|
||||||
)
|
)
|
||||||
|
|||||||
246
pkg/coredata/export_job.go
Normal file
246
pkg/coredata/export_job.go
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
package coredata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/getprobo/probo/pkg/gid"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"go.gearno.de/kit/pg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ExportJob struct {
|
||||||
|
ID gid.GID `db:"id"`
|
||||||
|
Type ExportJobType `db:"type"`
|
||||||
|
Arguments json.RawMessage `db:"arguments"`
|
||||||
|
Error *string `db:"error"`
|
||||||
|
Status ExportJobStatus `db:"status"`
|
||||||
|
FileID *gid.GID `db:"file_id"`
|
||||||
|
RecipientEmail string `db:"recipient_email"`
|
||||||
|
RecipientName string `db:"recipient_name"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
StartedAt *time.Time `db:"started_at"`
|
||||||
|
CompletedAt *time.Time `db:"completed_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ExportJobs []*ExportJob
|
||||||
|
|
||||||
|
DocumentExportArguments struct {
|
||||||
|
DocumentIDs []gid.GID `json:"document_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameworkExportArguments struct {
|
||||||
|
FrameworkID gid.GID `json:"framework_id"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoExportJobAvailable = errors.New("no export job available")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ej *ExportJob) Insert(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
INSERT INTO export_jobs (
|
||||||
|
id,
|
||||||
|
tenant_id,
|
||||||
|
type,
|
||||||
|
arguments,
|
||||||
|
status,
|
||||||
|
recipient_email,
|
||||||
|
recipient_name,
|
||||||
|
created_at
|
||||||
|
) VALUES (
|
||||||
|
@id,
|
||||||
|
@tenant_id,
|
||||||
|
@type,
|
||||||
|
@arguments,
|
||||||
|
@status,
|
||||||
|
@recipient_email,
|
||||||
|
@recipient_name,
|
||||||
|
@created_at
|
||||||
|
)`
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"id": ej.ID,
|
||||||
|
"tenant_id": scope.GetTenantID(),
|
||||||
|
"type": ej.Type,
|
||||||
|
"arguments": ej.Arguments,
|
||||||
|
"status": ej.Status,
|
||||||
|
"recipient_email": ej.RecipientEmail,
|
||||||
|
"recipient_name": ej.RecipientName,
|
||||||
|
"created_at": ej.CreatedAt,
|
||||||
|
}
|
||||||
|
_, err := conn.Exec(ctx, q, args)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ej *ExportJob) Update(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
UPDATE
|
||||||
|
export_jobs
|
||||||
|
SET
|
||||||
|
status = @status,
|
||||||
|
error = @error,
|
||||||
|
file_id = @file_id,
|
||||||
|
started_at = @started_at,
|
||||||
|
completed_at = @completed_at
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND id = @id
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"status": ej.Status,
|
||||||
|
"error": ej.Error,
|
||||||
|
"file_id": ej.FileID,
|
||||||
|
"started_at": ej.StartedAt,
|
||||||
|
"completed_at": ej.CompletedAt,
|
||||||
|
"id": ej.ID,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
_, err := conn.Exec(ctx, q, args)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ej *ExportJob) LoadByID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
id gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
arguments,
|
||||||
|
error,
|
||||||
|
status,
|
||||||
|
file_id,
|
||||||
|
recipient_email,
|
||||||
|
recipient_name,
|
||||||
|
created_at,
|
||||||
|
started_at,
|
||||||
|
completed_at
|
||||||
|
FROM
|
||||||
|
export_jobs
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND id = @id
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
args := pgx.StrictNamedArgs{"id": id}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ej2, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ExportJob])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect export job: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*ej = ej2
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ej *ExportJob) LoadNextPendingForUpdateSkipLocked(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
arguments,
|
||||||
|
error,
|
||||||
|
status,
|
||||||
|
file_id,
|
||||||
|
recipient_email,
|
||||||
|
recipient_name,
|
||||||
|
created_at,
|
||||||
|
started_at,
|
||||||
|
completed_at
|
||||||
|
FROM
|
||||||
|
export_jobs
|
||||||
|
WHERE
|
||||||
|
status = @status
|
||||||
|
ORDER BY
|
||||||
|
created_at ASC
|
||||||
|
LIMIT 1
|
||||||
|
FOR UPDATE SKIP LOCKED
|
||||||
|
`
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"status": ExportJobStatusPending,
|
||||||
|
}
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ej2, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ExportJob])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrNoExportJobAvailable
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot collect export job: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*ej = ej2
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ej *ExportJob) GetDocumentExportArguments() (*DocumentExportArguments, error) {
|
||||||
|
if ej.Type != ExportJobTypeDocument {
|
||||||
|
return nil, fmt.Errorf("export job is not a document export")
|
||||||
|
}
|
||||||
|
|
||||||
|
var args DocumentExportArguments
|
||||||
|
if err := json.Unmarshal(ej.Arguments, &args); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot unmarshal document export arguments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ej *ExportJob) GetFrameworkExportArguments() (*FrameworkExportArguments, error) {
|
||||||
|
if ej.Type != ExportJobTypeFramework {
|
||||||
|
return nil, fmt.Errorf("export job is not a framework export")
|
||||||
|
}
|
||||||
|
|
||||||
|
var args FrameworkExportArguments
|
||||||
|
if err := json.Unmarshal(ej.Arguments, &args); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot unmarshal framework export arguments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ej *ExportJob) GetDocumentIDs() ([]gid.GID, error) {
|
||||||
|
args, err := ej.GetDocumentExportArguments()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return args.DocumentIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ej *ExportJob) GetFrameworkID() (gid.GID, error) {
|
||||||
|
args, err := ej.GetFrameworkExportArguments()
|
||||||
|
if err != nil {
|
||||||
|
return gid.GID{}, err
|
||||||
|
}
|
||||||
|
return args.FrameworkID, nil
|
||||||
|
}
|
||||||
51
pkg/coredata/export_job_status.go
Normal file
51
pkg/coredata/export_job_status.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package coredata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ExportJobStatus string
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ExportJobStatusPending ExportJobStatus = "PENDING"
|
||||||
|
ExportJobStatusProcessing ExportJobStatus = "PROCESSING"
|
||||||
|
ExportJobStatusCompleted ExportJobStatus = "COMPLETED"
|
||||||
|
ExportJobStatusFailed ExportJobStatus = "FAILED"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ejs ExportJobStatus) String() string {
|
||||||
|
return string(ejs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ejs *ExportJobStatus) Scan(value any) error {
|
||||||
|
var s string
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
s = v
|
||||||
|
case []byte:
|
||||||
|
s = string(v)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported type for ExportJobStatus: %T", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case ExportJobStatusPending.String():
|
||||||
|
*ejs = ExportJobStatusPending
|
||||||
|
case ExportJobStatusProcessing.String():
|
||||||
|
*ejs = ExportJobStatusProcessing
|
||||||
|
case ExportJobStatusCompleted.String():
|
||||||
|
*ejs = ExportJobStatusCompleted
|
||||||
|
case ExportJobStatusFailed.String():
|
||||||
|
*ejs = ExportJobStatusFailed
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid ExportJobStatus value: %q", s)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ejs ExportJobStatus) Value() (driver.Value, error) {
|
||||||
|
return ejs.String(), nil
|
||||||
|
}
|
||||||
45
pkg/coredata/export_job_type.go
Normal file
45
pkg/coredata/export_job_type.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package coredata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ExportJobType string
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ExportJobTypeFramework ExportJobType = "FRAMEWORK"
|
||||||
|
ExportJobTypeDocument ExportJobType = "DOCUMENT"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ejt ExportJobType) String() string {
|
||||||
|
return string(ejt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ejt *ExportJobType) Scan(value any) error {
|
||||||
|
var s string
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
s = v
|
||||||
|
case []byte:
|
||||||
|
s = string(v)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported type for ExportJobType: %T", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case ExportJobTypeFramework.String():
|
||||||
|
*ejt = ExportJobTypeFramework
|
||||||
|
case ExportJobTypeDocument.String():
|
||||||
|
*ejt = ExportJobTypeDocument
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid ExportJobType value: %q", s)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ejt ExportJobType) Value() (driver.Value, error) {
|
||||||
|
return ejt.String(), nil
|
||||||
|
}
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
// 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 coredata
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"maps"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/getprobo/probo/pkg/gid"
|
|
||||||
"github.com/getprobo/probo/pkg/page"
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"go.gearno.de/kit/pg"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
FrameworkExport struct {
|
|
||||||
ID gid.GID `db:"id"`
|
|
||||||
FrameworkID gid.GID `db:"framework_id"`
|
|
||||||
RecipientEmail string `db:"recipient_email"`
|
|
||||||
RecipientName string `db:"recipient_name"`
|
|
||||||
Status FrameworkExportStatus `db:"status"`
|
|
||||||
FileID *gid.GID `db:"file_id"`
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
|
||||||
StartedAt *time.Time `db:"started_at"`
|
|
||||||
CompletedAt *time.Time `db:"completed_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
FrameworkExports []*FrameworkExport
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNoFrameworkExportAvailable = errors.New("no framework export available")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (fe FrameworkExport) CursorKey(orderBy FrameworkExportOrderField) page.CursorKey {
|
|
||||||
switch orderBy {
|
|
||||||
case FrameworkExportOrderFieldCreatedAt:
|
|
||||||
return page.NewCursorKey(fe.ID, fe.CreatedAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fe *FrameworkExport) Insert(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
INSERT INTO framework_exports (
|
|
||||||
id,
|
|
||||||
tenant_id,
|
|
||||||
framework_id,
|
|
||||||
recipient_email,
|
|
||||||
recipient_name,
|
|
||||||
status,
|
|
||||||
created_at
|
|
||||||
) VALUES (
|
|
||||||
@id,
|
|
||||||
@tenant_id,
|
|
||||||
@framework_id,
|
|
||||||
@recipient_email,
|
|
||||||
@recipient_name,
|
|
||||||
@status,
|
|
||||||
@created_at
|
|
||||||
)`
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"id": fe.ID,
|
|
||||||
"tenant_id": scope.GetTenantID(),
|
|
||||||
"framework_id": fe.FrameworkID,
|
|
||||||
"recipient_email": fe.RecipientEmail,
|
|
||||||
"recipient_name": fe.RecipientName,
|
|
||||||
"status": fe.Status,
|
|
||||||
"created_at": fe.CreatedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, q, args)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fe *FrameworkExport) Update(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
UPDATE
|
|
||||||
framework_exports
|
|
||||||
SET
|
|
||||||
status = @status,
|
|
||||||
file_id = @file_id,
|
|
||||||
started_at = @started_at,
|
|
||||||
completed_at = @completed_at
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND id = @id
|
|
||||||
`
|
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"status": fe.Status,
|
|
||||||
"file_id": fe.FileID,
|
|
||||||
"started_at": fe.StartedAt,
|
|
||||||
"completed_at": fe.CompletedAt,
|
|
||||||
"id": fe.ID,
|
|
||||||
}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, q, args)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fe *FrameworkExport) LoadNextPendingForUpdateSkipLocked(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
framework_id,
|
|
||||||
recipient_email,
|
|
||||||
recipient_name,
|
|
||||||
status,
|
|
||||||
file_id,
|
|
||||||
created_at,
|
|
||||||
started_at,
|
|
||||||
completed_at
|
|
||||||
FROM
|
|
||||||
framework_exports
|
|
||||||
WHERE
|
|
||||||
status = @status
|
|
||||||
ORDER BY
|
|
||||||
created_at ASC
|
|
||||||
LIMIT 1
|
|
||||||
FOR UPDATE SKIP LOCKED
|
|
||||||
`
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"status": FrameworkExportStatusPending}
|
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fe2, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FrameworkExport])
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return ErrNoFrameworkExportAvailable
|
|
||||||
}
|
|
||||||
return fmt.Errorf("cannot collect framework export: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*fe = fe2
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
// 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 coredata
|
|
||||||
|
|
||||||
type (
|
|
||||||
FrameworkExportOrderField string
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
FrameworkExportOrderFieldCreatedAt FrameworkExportOrderField = "CREATED_AT"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p FrameworkExportOrderField) Column() string {
|
|
||||||
return string(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p FrameworkExportOrderField) String() string {
|
|
||||||
return string(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p FrameworkExportOrderField) MarshalText() ([]byte, error) {
|
|
||||||
return []byte(p.String()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *FrameworkExportOrderField) UnmarshalText(text []byte) error {
|
|
||||||
*p = FrameworkExportOrderField(text)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
// 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 coredata
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql/driver"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
FrameworkExportStatus string
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
FrameworkExportStatusPending FrameworkExportStatus = "pending"
|
|
||||||
FrameworkExportStatusProcessing FrameworkExportStatus = "processing"
|
|
||||||
FrameworkExportStatusCompleted FrameworkExportStatus = "completed"
|
|
||||||
FrameworkExportStatusFailed FrameworkExportStatus = "failed"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (pvs FrameworkExportStatus) MarshalText() ([]byte, error) {
|
|
||||||
return []byte(pvs.String()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pvs *FrameworkExportStatus) UnmarshalText(data []byte) error {
|
|
||||||
val := string(data)
|
|
||||||
|
|
||||||
switch val {
|
|
||||||
case FrameworkExportStatusPending.String():
|
|
||||||
*pvs = FrameworkExportStatusPending
|
|
||||||
case FrameworkExportStatusProcessing.String():
|
|
||||||
*pvs = FrameworkExportStatusProcessing
|
|
||||||
case FrameworkExportStatusCompleted.String():
|
|
||||||
*pvs = FrameworkExportStatusCompleted
|
|
||||||
case FrameworkExportStatusFailed.String():
|
|
||||||
*pvs = FrameworkExportStatusFailed
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid FrameworkExportStatus value: %q", val)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pvs FrameworkExportStatus) String() string {
|
|
||||||
var val string
|
|
||||||
|
|
||||||
switch pvs {
|
|
||||||
case FrameworkExportStatusPending:
|
|
||||||
val = "pending"
|
|
||||||
case FrameworkExportStatusProcessing:
|
|
||||||
val = "processing"
|
|
||||||
case FrameworkExportStatusCompleted:
|
|
||||||
val = "completed"
|
|
||||||
case FrameworkExportStatusFailed:
|
|
||||||
val = "failed"
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("invalid FrameworkExportStatus value: %q", string(pvs)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pvs *FrameworkExportStatus) Scan(value any) error {
|
|
||||||
val, ok := value.(string)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid scan source for FrameworkExportStatus, expected string got %T", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pvs.UnmarshalText([]byte(val))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pvs FrameworkExportStatus) Value() (driver.Value, error) {
|
|
||||||
return pvs.String(), nil
|
|
||||||
}
|
|
||||||
34
pkg/coredata/migrations/20250916T121639Z.sql
Normal file
34
pkg/coredata/migrations/20250916T121639Z.sql
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
CREATE TYPE export_jobs_status AS ENUM (
|
||||||
|
'PENDING',
|
||||||
|
'PROCESSING',
|
||||||
|
'COMPLETED',
|
||||||
|
'FAILED'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TYPE export_jobs_type AS ENUM (
|
||||||
|
'FRAMEWORK',
|
||||||
|
'DOCUMENT'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE export_jobs (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
tenant_id TEXT NOT NULL,
|
||||||
|
type export_jobs_type NOT NULL,
|
||||||
|
arguments JSONB NOT NULL,
|
||||||
|
error TEXT,
|
||||||
|
status export_jobs_status NOT NULL,
|
||||||
|
file_id TEXT,
|
||||||
|
recipient_email TEXT NOT NULL,
|
||||||
|
recipient_name TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
started_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
completed_at TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE export_jobs ADD CONSTRAINT export_jobs_file_id_fkey
|
||||||
|
FOREIGN KEY (file_id)
|
||||||
|
REFERENCES files(id)
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
|
||||||
|
DROP TABLE framework_exports;
|
||||||
@@ -1,12 +1,19 @@
|
|||||||
package probo
|
package probo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
"github.com/getprobo/probo/pkg/coredata"
|
"github.com/getprobo/probo/pkg/coredata"
|
||||||
"github.com/getprobo/probo/pkg/docgen"
|
"github.com/getprobo/probo/pkg/docgen"
|
||||||
"github.com/getprobo/probo/pkg/gid"
|
"github.com/getprobo/probo/pkg/gid"
|
||||||
@@ -14,7 +21,9 @@ import (
|
|||||||
"github.com/getprobo/probo/pkg/page"
|
"github.com/getprobo/probo/pkg/page"
|
||||||
"github.com/getprobo/probo/pkg/statelesstoken"
|
"github.com/getprobo/probo/pkg/statelesstoken"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
|
"go.gearno.de/crypto/uuid"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.gearno.de/x/ref"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -68,6 +77,22 @@ type (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
TokenTypeSigningRequest = "signing_request"
|
TokenTypeSigningRequest = "signing_request"
|
||||||
|
|
||||||
|
documentExportEmailExpiresIn = 24 * time.Hour
|
||||||
|
documentExportEmailSubject = "Your document export is ready"
|
||||||
|
documentExportEmailBody = `
|
||||||
|
Your document export has been completed successfully.
|
||||||
|
|
||||||
|
You can download the export using the link below:
|
||||||
|
[1] %s
|
||||||
|
|
||||||
|
This link will expire in 24 hours.`
|
||||||
|
|
||||||
|
maxFilenameLength = 200
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
invalidFilenameChars = regexp.MustCompile(`[<>:"/\\|?*\x00-\x1f\x7f]`)
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e ErrSignatureNotCancellable) Error() string {
|
func (e ErrSignatureNotCancellable) Error() string {
|
||||||
@@ -814,6 +839,76 @@ func (s *DocumentService) SoftDelete(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DocumentService) BulkSoftDelete(
|
||||||
|
ctx context.Context,
|
||||||
|
documentIDs []gid.GID,
|
||||||
|
) error {
|
||||||
|
documents := coredata.Documents{}
|
||||||
|
|
||||||
|
for _, documentID := range documentIDs {
|
||||||
|
documents = append(documents, &coredata.Document{ID: documentID})
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.svc.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
return documents.BulkSoftDelete(ctx, conn, s.svc.scope)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DocumentService) RequestExport(
|
||||||
|
ctx context.Context,
|
||||||
|
documentIDs []gid.GID,
|
||||||
|
recipientEmail string,
|
||||||
|
recipientName string,
|
||||||
|
) (*coredata.ExportJob, error) {
|
||||||
|
var exportJobID gid.GID
|
||||||
|
exportJob := &coredata.ExportJob{}
|
||||||
|
|
||||||
|
err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {
|
||||||
|
for _, documentID := range documentIDs {
|
||||||
|
document := &coredata.Document{}
|
||||||
|
if err := document.LoadByID(ctx, conn, s.svc.scope, documentID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load document %q: %w", documentID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
exportJobID = gid.New(s.svc.scope.GetTenantID(), coredata.ExportJobEntityType)
|
||||||
|
|
||||||
|
args := coredata.DocumentExportArguments{
|
||||||
|
DocumentIDs: documentIDs,
|
||||||
|
}
|
||||||
|
argsJSON, err := json.Marshal(args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot marshal document export arguments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exportJob = &coredata.ExportJob{
|
||||||
|
ID: exportJobID,
|
||||||
|
Type: coredata.ExportJobTypeDocument,
|
||||||
|
Arguments: argsJSON,
|
||||||
|
Status: coredata.ExportJobStatusPending,
|
||||||
|
RecipientEmail: recipientEmail,
|
||||||
|
RecipientName: recipientName,
|
||||||
|
CreatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := exportJob.Insert(ctx, conn, s.svc.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot insert export job: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return exportJob, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *DocumentService) ListVersions(
|
func (s *DocumentService) ListVersions(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
documentID gid.GID,
|
documentID gid.GID,
|
||||||
@@ -1118,6 +1213,97 @@ func (s *DocumentService) ExportPDF(
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DocumentService) BuildAndUploadExport(ctx context.Context, exportJobID gid.GID) (*coredata.ExportJob, error) {
|
||||||
|
exportJob := &coredata.ExportJob{}
|
||||||
|
err := s.svc.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
if err := exportJob.LoadByID(ctx, tx, s.svc.scope, exportJobID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load export job: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
documentIDs, err := exportJob.GetDocumentIDs()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot get document IDs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
tempFile, err := os.CreateTemp(tempDir, "probo-document-export-*.zip")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create temp file: %w", err)
|
||||||
|
}
|
||||||
|
defer tempFile.Close()
|
||||||
|
defer os.Remove(tempFile.Name())
|
||||||
|
|
||||||
|
err = s.Export(ctx, documentIDs, tempFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot export documents: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid, err := uuid.NewV4()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot generate uuid: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := tempFile.Seek(0, 0); err != nil {
|
||||||
|
return fmt.Errorf("cannot seek temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfo, err := tempFile.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot stat temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.svc.s3.PutObject(
|
||||||
|
ctx,
|
||||||
|
&s3.PutObjectInput{
|
||||||
|
Bucket: ref.Ref(s.svc.bucket),
|
||||||
|
Key: ref.Ref(uuid.String()),
|
||||||
|
Body: tempFile,
|
||||||
|
ContentLength: ref.Ref(fileInfo.Size()),
|
||||||
|
ContentType: ref.Ref("application/zip"),
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"type": "document-export",
|
||||||
|
"export-job-id": exportJob.ID.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot upload file to S3: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
file := coredata.File{
|
||||||
|
ID: gid.New(exportJob.ID.TenantID(), coredata.FileEntityType),
|
||||||
|
BucketName: s.svc.bucket,
|
||||||
|
MimeType: "application/zip",
|
||||||
|
FileName: fmt.Sprintf("Documents Export %s.zip", now.Format("2006-01-02")),
|
||||||
|
FileKey: uuid.String(),
|
||||||
|
FileSize: int(fileInfo.Size()),
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := file.Insert(ctx, tx, s.svc.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot insert file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exportJob.FileID = &file.ID
|
||||||
|
if err := exportJob.Update(ctx, tx, s.svc.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot update export job: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return exportJob, nil
|
||||||
|
}
|
||||||
|
|
||||||
func exportDocumentPDF(
|
func exportDocumentPDF(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
html2pdfConverter *html2pdf.Converter,
|
html2pdfConverter *html2pdf.Converter,
|
||||||
@@ -1240,3 +1426,146 @@ func exportDocumentPDF(
|
|||||||
}
|
}
|
||||||
return pdfData, nil
|
return pdfData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DocumentService) Export(
|
||||||
|
ctx context.Context,
|
||||||
|
documentIDs []gid.GID,
|
||||||
|
file io.Writer,
|
||||||
|
) (err error) {
|
||||||
|
archive := zip.NewWriter(file)
|
||||||
|
defer func() {
|
||||||
|
if closeErr := archive.Close(); closeErr != nil && err == nil {
|
||||||
|
err = fmt.Errorf("cannot close archive: %w", closeErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return s.svc.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
for i, documentID := range documentIDs {
|
||||||
|
document := &coredata.Document{}
|
||||||
|
if err := document.LoadByID(ctx, conn, s.svc.scope, documentID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load document %q: %w", documentID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
documentVersion := &coredata.DocumentVersion{}
|
||||||
|
if err := documentVersion.LoadLatestVersion(ctx, conn, s.svc.scope, documentID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load document version for %q: %w", documentID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exportedPDF, err := exportDocumentPDF(
|
||||||
|
ctx,
|
||||||
|
s.html2pdfConverter,
|
||||||
|
conn,
|
||||||
|
s.svc.scope,
|
||||||
|
documentVersion.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot export document PDF for %q: %w", documentID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("%d_%s.pdf", i+1, sanitizeFilename(document.Title))
|
||||||
|
w, err := archive.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create document in archive: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write(exportedPDF)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot write document to archive: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DocumentService) SendExportEmail(
|
||||||
|
ctx context.Context,
|
||||||
|
fileID gid.GID,
|
||||||
|
recipientName string,
|
||||||
|
recipientEmail string,
|
||||||
|
) error {
|
||||||
|
return s.svc.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
file := &coredata.File{}
|
||||||
|
if err := file.LoadByID(ctx, tx, s.svc.scope, fileID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadURL, err := s.GenerateDocumentExportDownloadURL(ctx, file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot generate download URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
email := coredata.NewEmail(
|
||||||
|
recipientName,
|
||||||
|
recipientEmail,
|
||||||
|
documentExportEmailSubject,
|
||||||
|
fmt.Sprintf(documentExportEmailBody, downloadURL),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := email.Insert(ctx, tx); err != nil {
|
||||||
|
return fmt.Errorf("cannot insert email: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DocumentService) GenerateDocumentExportDownloadURL(
|
||||||
|
ctx context.Context,
|
||||||
|
file *coredata.File,
|
||||||
|
) (string, error) {
|
||||||
|
presignClient := s3.NewPresignClient(s.svc.s3)
|
||||||
|
|
||||||
|
presignedReq, err := presignClient.PresignGetObject(
|
||||||
|
ctx,
|
||||||
|
&s3.GetObjectInput{
|
||||||
|
Bucket: ref.Ref(s.svc.bucket),
|
||||||
|
Key: ref.Ref(file.FileKey),
|
||||||
|
ResponseCacheControl: ref.Ref("max-age=3600, public"),
|
||||||
|
ResponseContentType: ref.Ref(file.MimeType),
|
||||||
|
ResponseContentDisposition: ref.Ref(fmt.Sprintf("attachment; filename=\"%s\"", file.FileName)),
|
||||||
|
},
|
||||||
|
func(opts *s3.PresignOptions) {
|
||||||
|
opts.Expires = documentExportEmailExpiresIn
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("cannot presign GetObject request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return presignedReq.URL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeFilename(title string) string {
|
||||||
|
if title == "" {
|
||||||
|
return "Untitled"
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitized := invalidFilenameChars.ReplaceAllString(title, "_")
|
||||||
|
|
||||||
|
sanitized = strings.TrimFunc(sanitized, func(r rune) bool {
|
||||||
|
return unicode.IsSpace(r) || r == '.'
|
||||||
|
})
|
||||||
|
|
||||||
|
sanitized = regexp.MustCompile(`[\s_]+`).ReplaceAllString(sanitized, "_")
|
||||||
|
|
||||||
|
if sanitized == "" || sanitized == "_" {
|
||||||
|
sanitized = "Untitled"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sanitized) > maxFilenameLength-20 {
|
||||||
|
sanitized = sanitized[:maxFilenameLength-20]
|
||||||
|
sanitized = strings.TrimFunc(sanitized, func(r rune) bool {
|
||||||
|
return r == unicode.ReplacementChar
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitized
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ package probo
|
|||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
@@ -29,6 +31,7 @@ import (
|
|||||||
"github.com/getprobo/probo/pkg/page"
|
"github.com/getprobo/probo/pkg/page"
|
||||||
"github.com/getprobo/probo/pkg/slug"
|
"github.com/getprobo/probo/pkg/slug"
|
||||||
"github.com/getprobo/probo/pkg/soagen"
|
"github.com/getprobo/probo/pkg/soagen"
|
||||||
|
"go.gearno.de/crypto/uuid"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
"go.gearno.de/x/ref"
|
"go.gearno.de/x/ref"
|
||||||
)
|
)
|
||||||
@@ -82,8 +85,9 @@ func (s FrameworkService) RequestExport(
|
|||||||
frameworkID gid.GID,
|
frameworkID gid.GID,
|
||||||
recipientEmail string,
|
recipientEmail string,
|
||||||
recipientName string,
|
recipientName string,
|
||||||
) (error, *coredata.FrameworkExport) {
|
) (error, *coredata.ExportJob) {
|
||||||
frameworkExport := &coredata.FrameworkExport{}
|
var exportJobID gid.GID
|
||||||
|
exportJob := &coredata.ExportJob{}
|
||||||
|
|
||||||
err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {
|
err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {
|
||||||
framework := &coredata.Framework{}
|
framework := &coredata.Framework{}
|
||||||
@@ -92,18 +96,28 @@ func (s FrameworkService) RequestExport(
|
|||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
exportJobID = gid.New(s.svc.scope.GetTenantID(), coredata.ExportJobEntityType)
|
||||||
|
|
||||||
frameworkExport = &coredata.FrameworkExport{
|
args := coredata.FrameworkExportArguments{
|
||||||
ID: gid.New(framework.ID.TenantID(), coredata.FrameworkExportEntityType),
|
|
||||||
FrameworkID: frameworkID,
|
FrameworkID: frameworkID,
|
||||||
Status: coredata.FrameworkExportStatusPending,
|
}
|
||||||
|
argsJSON, err := json.Marshal(args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot marshal framework export arguments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exportJob = &coredata.ExportJob{
|
||||||
|
ID: exportJobID,
|
||||||
|
Type: coredata.ExportJobTypeFramework,
|
||||||
|
Arguments: argsJSON,
|
||||||
|
Status: coredata.ExportJobStatusPending,
|
||||||
RecipientEmail: recipientEmail,
|
RecipientEmail: recipientEmail,
|
||||||
RecipientName: recipientName,
|
RecipientName: recipientName,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := frameworkExport.Insert(ctx, conn, s.svc.scope); err != nil {
|
if err := exportJob.Insert(ctx, conn, s.svc.scope); err != nil {
|
||||||
return fmt.Errorf("cannot insert framework export: %w", err)
|
return fmt.Errorf("cannot insert export job: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -113,7 +127,7 @@ func (s FrameworkService) RequestExport(
|
|||||||
return err, nil
|
return err, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, frameworkExport
|
return nil, exportJob
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s FrameworkService) Export(
|
func (s FrameworkService) Export(
|
||||||
@@ -675,7 +689,7 @@ func (s FrameworkService) StateOfApplicability(ctx context.Context, frameworkID
|
|||||||
return output, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s FrameworkService) SendFrameworkExportEmail(
|
func (s FrameworkService) SendExportEmail(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
fileID gid.GID,
|
fileID gid.GID,
|
||||||
recipientName string,
|
recipientName string,
|
||||||
@@ -736,3 +750,99 @@ func (s FrameworkService) GenerateFrameworkExportDownloadURL(
|
|||||||
|
|
||||||
return presignedReq.URL, nil
|
return presignedReq.URL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *FrameworkService) BuildAndUploadExport(ctx context.Context, exportJobID gid.GID) (*coredata.ExportJob, error) {
|
||||||
|
exportJob := &coredata.ExportJob{}
|
||||||
|
err := s.svc.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
if err := exportJob.LoadByID(ctx, tx, s.svc.scope, exportJobID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load export job: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
frameworkID, err := exportJob.GetFrameworkID()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot get framework ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
framework := &coredata.Framework{}
|
||||||
|
if err := framework.LoadByID(ctx, tx, s.svc.scope, frameworkID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load framework: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
tempFile, err := os.CreateTemp(tempDir, "probo-framework-export-*.zip")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create temp file: %w", err)
|
||||||
|
}
|
||||||
|
defer tempFile.Close()
|
||||||
|
defer os.Remove(tempFile.Name())
|
||||||
|
|
||||||
|
err = s.Export(ctx, frameworkID, tempFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot export framework: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid, err := uuid.NewV4()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot generate uuid: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := tempFile.Seek(0, 0); err != nil {
|
||||||
|
return fmt.Errorf("cannot seek temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfo, err := tempFile.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot stat temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.svc.s3.PutObject(
|
||||||
|
ctx,
|
||||||
|
&s3.PutObjectInput{
|
||||||
|
Bucket: ref.Ref(s.svc.bucket),
|
||||||
|
Key: ref.Ref(uuid.String()),
|
||||||
|
Body: tempFile,
|
||||||
|
ContentLength: ref.Ref(fileInfo.Size()),
|
||||||
|
ContentType: ref.Ref("application/zip"),
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"type": "framework-export",
|
||||||
|
"export-job-id": exportJob.ID.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot upload file to S3: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
file := coredata.File{
|
||||||
|
ID: gid.New(exportJob.ID.TenantID(), coredata.FileEntityType),
|
||||||
|
BucketName: s.svc.bucket,
|
||||||
|
MimeType: "application/zip",
|
||||||
|
FileName: fmt.Sprintf("Framework Export %s.zip", now.Format("2006-01-02")),
|
||||||
|
FileKey: uuid.String(),
|
||||||
|
FileSize: int(fileInfo.Size()),
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := file.Insert(ctx, tx, s.svc.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot insert file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exportJob.FileID = &file.ID
|
||||||
|
if err := exportJob.Update(ctx, tx, s.svc.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot update export job: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return exportJob, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ package probo
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
@@ -28,11 +27,15 @@ import (
|
|||||||
"github.com/getprobo/probo/pkg/gid"
|
"github.com/getprobo/probo/pkg/gid"
|
||||||
"github.com/getprobo/probo/pkg/html2pdf"
|
"github.com/getprobo/probo/pkg/html2pdf"
|
||||||
"github.com/getprobo/probo/pkg/usrmgr"
|
"github.com/getprobo/probo/pkg/usrmgr"
|
||||||
"go.gearno.de/crypto/uuid"
|
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
"go.gearno.de/x/ref"
|
"go.gearno.de/x/ref"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ExportService interface {
|
||||||
|
BuildAndUploadExport(ctx context.Context, exportJobID gid.GID) (*coredata.ExportJob, error)
|
||||||
|
SendExportEmail(ctx context.Context, fileID gid.GID, recipientName, recipientEmail string) error
|
||||||
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
TrustConfig struct {
|
TrustConfig struct {
|
||||||
TokenSecret string
|
TokenSecret string
|
||||||
@@ -191,170 +194,100 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
|
|||||||
return tenantService
|
return tenantService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ExportFrameworkJob(ctx context.Context) error {
|
func (s *Service) ExportJob(ctx context.Context) error {
|
||||||
fe, scope, err := s.lockExport(ctx)
|
exportJob, err := s.lockExportJob(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot lock framework export: %w", err)
|
return fmt.Errorf("cannot lock export job: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fe, buildErr := s.buildAndUploadExport(ctx, scope, fe)
|
tenantService := s.WithTenant(exportJob.ID.TenantID())
|
||||||
|
|
||||||
|
var exportService ExportService
|
||||||
|
|
||||||
|
switch exportJob.Type {
|
||||||
|
case coredata.ExportJobTypeFramework:
|
||||||
|
exportService = tenantService.Frameworks
|
||||||
|
case coredata.ExportJobTypeDocument:
|
||||||
|
exportService = tenantService.Documents
|
||||||
|
default:
|
||||||
|
unknownTypeErr := fmt.Errorf("unknown export job type: %q", exportJob.Type)
|
||||||
|
if err := s.commitFailedExport(ctx, exportJob, unknownTypeErr); err != nil {
|
||||||
|
return fmt.Errorf("unknown export job type %q, and cannot commit failed export: %w", exportJob.Type, err)
|
||||||
|
}
|
||||||
|
return unknownTypeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
exportJob, buildErr := exportService.BuildAndUploadExport(ctx, exportJob.ID)
|
||||||
if buildErr != nil {
|
if buildErr != nil {
|
||||||
if err := s.commitFailedExport(ctx, scope, fe); err != nil {
|
if err := s.commitFailedExport(ctx, exportJob, buildErr); err != nil {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"cannot build and upload framework export: %w, and cannot commit failed export: %w",
|
"cannot build and upload %s export: %w, and cannot commit failed export: %w",
|
||||||
|
exportJob.Type,
|
||||||
buildErr,
|
buildErr,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
return fmt.Errorf("cannot build and upload %s export: %w", exportJob.Type, buildErr)
|
||||||
return fmt.Errorf("cannot build and upload framework export: %w", buildErr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tenantService := s.WithTenant(scope.GetTenantID())
|
if emailErr := exportService.SendExportEmail(ctx, *exportJob.FileID, exportJob.RecipientName, exportJob.RecipientEmail); emailErr != nil {
|
||||||
if emailErr := tenantService.Frameworks.SendFrameworkExportEmail(ctx, *fe.FileID, fe.RecipientName, fe.RecipientEmail); emailErr != nil {
|
if err := s.commitFailedExport(ctx, exportJob, emailErr); err != nil {
|
||||||
if err := s.commitFailedExport(ctx, scope, fe); err != nil {
|
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"cannot send completion email: %w, and cannot commit failed export: %w",
|
"cannot send completion email: %w, and cannot commit failed export: %w",
|
||||||
emailErr,
|
emailErr,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("cannot send completion email: %w", emailErr)
|
return fmt.Errorf("cannot send completion email: %w", emailErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.commitSuccessfulExport(ctx, scope, fe); err != nil {
|
if err := s.commitSuccessfulExport(ctx, exportJob); err != nil {
|
||||||
return fmt.Errorf("cannot commit successful export: %w", err)
|
return fmt.Errorf("cannot commit successful %s export: %w", exportJob.Type, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) lockExport(ctx context.Context) (*coredata.FrameworkExport, coredata.Scoper, error) {
|
func (s *Service) lockExportJob(ctx context.Context) (*coredata.ExportJob, error) {
|
||||||
fe := &coredata.FrameworkExport{}
|
exportJob := &coredata.ExportJob{}
|
||||||
var scope coredata.Scoper
|
var scope coredata.Scoper
|
||||||
|
|
||||||
err := s.pg.WithTx(ctx,
|
err := s.pg.WithTx(ctx,
|
||||||
func(tx pg.Conn) error {
|
func(tx pg.Conn) error {
|
||||||
if err := fe.LoadNextPendingForUpdateSkipLocked(ctx, tx); err != nil {
|
if err := exportJob.LoadNextPendingForUpdateSkipLocked(ctx, tx); err != nil {
|
||||||
return fmt.Errorf("cannot load next pending framework export: %w", err)
|
return fmt.Errorf("cannot load next pending export job: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
scope = coredata.NewScope(fe.ID.TenantID())
|
scope = coredata.NewScope(exportJob.ID.TenantID())
|
||||||
|
|
||||||
fe.Status = coredata.FrameworkExportStatusProcessing
|
exportJob.Status = coredata.ExportJobStatusProcessing
|
||||||
fe.StartedAt = ref.Ref(time.Now())
|
exportJob.StartedAt = ref.Ref(time.Now())
|
||||||
if err := fe.Update(ctx, tx, scope); err != nil {
|
if err := exportJob.Update(ctx, tx, scope); err != nil {
|
||||||
return fmt.Errorf("cannot update framework export: %w", err)
|
return fmt.Errorf("cannot update %s export job: %w", exportJob.Type, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("cannot lock framework export: %w", err)
|
return nil, fmt.Errorf("cannot lock export job: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fe, scope, nil
|
return exportJob, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) buildAndUploadExport(ctx context.Context, scope coredata.Scoper, fe *coredata.FrameworkExport) (*coredata.FrameworkExport, error) {
|
func (s *Service) commitFailedExport(ctx context.Context, exportJob *coredata.ExportJob, failureErr error) error {
|
||||||
err := s.pg.WithTx(
|
exportJob.CompletedAt = ref.Ref(time.Now())
|
||||||
ctx,
|
exportJob.Status = coredata.ExportJobStatusFailed
|
||||||
func(tx pg.Conn) error {
|
errorMsg := failureErr.Error()
|
||||||
framework := &coredata.Framework{}
|
exportJob.Error = &errorMsg
|
||||||
if err := framework.LoadByID(ctx, tx, scope, fe.FrameworkID); err != nil {
|
|
||||||
return fmt.Errorf("cannot load framework: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tempDir := os.TempDir()
|
|
||||||
tempFile, err := os.CreateTemp(tempDir, "probo-framework-export-*.zip")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot create temp file: %w", err)
|
|
||||||
}
|
|
||||||
defer tempFile.Close()
|
|
||||||
defer os.Remove(tempFile.Name())
|
|
||||||
|
|
||||||
tenantService := s.WithTenant(scope.GetTenantID())
|
|
||||||
err = tenantService.Frameworks.Export(ctx, fe.FrameworkID, tempFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot export framework: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
uuid, err := uuid.NewV4()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot generate uuid: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := tempFile.Seek(0, 0); err != nil {
|
|
||||||
return fmt.Errorf("cannot seek temp file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInfo, err := tempFile.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot stat temp file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.s3.PutObject(
|
|
||||||
ctx,
|
|
||||||
&s3.PutObjectInput{
|
|
||||||
Bucket: ref.Ref(s.bucket),
|
|
||||||
Key: ref.Ref(uuid.String()),
|
|
||||||
Body: tempFile,
|
|
||||||
ContentLength: ref.Ref(fileInfo.Size()),
|
|
||||||
ContentType: ref.Ref("application/zip"),
|
|
||||||
Metadata: map[string]string{
|
|
||||||
"framework-id": framework.ID.String(),
|
|
||||||
"framework-export-id": fe.ID.String(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot upload file to S3: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
file := coredata.File{
|
|
||||||
ID: gid.New(fe.ID.TenantID(), coredata.FileEntityType),
|
|
||||||
BucketName: s.bucket,
|
|
||||||
MimeType: "application/zip",
|
|
||||||
FileName: fmt.Sprintf("%s Archive %s.zip", framework.Name, now.Format("2006-01-02")),
|
|
||||||
FileKey: uuid.String(),
|
|
||||||
FileSize: int(fileInfo.Size()),
|
|
||||||
CreatedAt: now,
|
|
||||||
UpdatedAt: now,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := file.Insert(ctx, tx, scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot insert file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fe.FileID = &file.ID
|
|
||||||
if err := fe.Update(ctx, tx, scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot update framework export: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fe, fmt.Errorf("cannot build and upload export: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fe, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) commitFailedExport(ctx context.Context, scope coredata.Scoper, fe *coredata.FrameworkExport) error {
|
|
||||||
fe.CompletedAt = ref.Ref(time.Now())
|
|
||||||
fe.Status = coredata.FrameworkExportStatusFailed
|
|
||||||
|
|
||||||
return s.pg.WithConn(
|
return s.pg.WithConn(
|
||||||
ctx,
|
ctx,
|
||||||
func(conn pg.Conn) error {
|
func(conn pg.Conn) error {
|
||||||
if err := fe.Update(ctx, conn, scope); err != nil {
|
scope := coredata.NewScope(exportJob.ID.TenantID())
|
||||||
return fmt.Errorf("cannot update framework export: %w", err)
|
if err := exportJob.Update(ctx, conn, scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot update %s export job: %w", exportJob.Type, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -362,15 +295,16 @@ func (s *Service) commitFailedExport(ctx context.Context, scope coredata.Scoper,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) commitSuccessfulExport(ctx context.Context, scope coredata.Scoper, fe *coredata.FrameworkExport) error {
|
func (s *Service) commitSuccessfulExport(ctx context.Context, exportJob *coredata.ExportJob) error {
|
||||||
fe.CompletedAt = ref.Ref(time.Now())
|
exportJob.CompletedAt = ref.Ref(time.Now())
|
||||||
fe.Status = coredata.FrameworkExportStatusCompleted
|
exportJob.Status = coredata.ExportJobStatusCompleted
|
||||||
|
|
||||||
return s.pg.WithConn(
|
return s.pg.WithConn(
|
||||||
ctx,
|
ctx,
|
||||||
func(conn pg.Conn) error {
|
func(conn pg.Conn) error {
|
||||||
if err := fe.Update(ctx, conn, scope); err != nil {
|
scope := coredata.NewScope(exportJob.ID.TenantID())
|
||||||
return fmt.Errorf("cannot update framework export: %w", err)
|
if err := exportJob.Update(ctx, conn, scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot update %s export job: %w", exportJob.Type, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -330,19 +330,19 @@ func (impl *Implm) Run(
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
frameworkExporterCtx, stopFrameworkExporter := context.WithCancel(context.Background())
|
exportJobExporterCtx, stopExportJobExporter := context.WithCancel(context.Background())
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if err := impl.runFrameworkExporter(frameworkExporterCtx, proboService, l.Named("framework-exporter")); err != nil {
|
if err := impl.runExportJob(exportJobExporterCtx, proboService, l.Named("export-job-exporter")); err != nil {
|
||||||
cancel(fmt.Errorf("framework exporter crashed: %w", err))
|
cancel(fmt.Errorf("export job exporter crashed: %w", err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
|
|
||||||
stopMailer()
|
stopMailer()
|
||||||
stopFrameworkExporter()
|
stopExportJobExporter()
|
||||||
stopApiServer()
|
stopApiServer()
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -352,7 +352,7 @@ func (impl *Implm) Run(
|
|||||||
return context.Cause(ctx)
|
return context.Cause(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (impl *Implm) runFrameworkExporter(
|
func (impl *Implm) runExportJob(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
proboService *probo.Service,
|
proboService *probo.Service,
|
||||||
l *log.Logger,
|
l *log.Logger,
|
||||||
@@ -362,9 +362,9 @@ LOOP:
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
case <-time.After(30 * time.Second):
|
case <-time.After(30 * time.Second):
|
||||||
if err := proboService.ExportFrameworkJob(ctx); err != nil {
|
if err := proboService.ExportJob(ctx); err != nil {
|
||||||
if !errors.Is(err, coredata.ErrNoFrameworkExportAvailable) {
|
if !errors.Is(err, coredata.ErrNoExportJobAvailable) {
|
||||||
l.ErrorCtx(ctx, "cannot process framework export", log.Error(err))
|
l.ErrorCtx(ctx, "cannot process export job", log.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2643,6 +2643,12 @@ type Mutation {
|
|||||||
bulkPublishDocumentVersions(
|
bulkPublishDocumentVersions(
|
||||||
input: BulkPublishDocumentVersionsInput!
|
input: BulkPublishDocumentVersionsInput!
|
||||||
): BulkPublishDocumentVersionsPayload!
|
): BulkPublishDocumentVersionsPayload!
|
||||||
|
bulkDeleteDocuments(
|
||||||
|
input: BulkDeleteDocumentsInput!
|
||||||
|
): BulkDeleteDocumentsPayload!
|
||||||
|
bulkExportDocuments(
|
||||||
|
input: BulkExportDocumentsInput!
|
||||||
|
): BulkExportDocumentsPayload!
|
||||||
generateDocumentChangelog(
|
generateDocumentChangelog(
|
||||||
input: GenerateDocumentChangelogInput!
|
input: GenerateDocumentChangelogInput!
|
||||||
): GenerateDocumentChangelogPayload!
|
): GenerateDocumentChangelogPayload!
|
||||||
@@ -3856,6 +3862,22 @@ type BulkPublishDocumentVersionsPayload {
|
|||||||
documentEdges: [DocumentEdge!]!
|
documentEdges: [DocumentEdge!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input BulkDeleteDocumentsInput {
|
||||||
|
documentIds: [ID!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
input BulkExportDocumentsInput {
|
||||||
|
documentIds: [ID!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type BulkDeleteDocumentsPayload {
|
||||||
|
deletedDocumentIds: [ID!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type BulkExportDocumentsPayload {
|
||||||
|
exportJobId: ID!
|
||||||
|
}
|
||||||
|
|
||||||
input PublishDocumentVersionInput {
|
input PublishDocumentVersionInput {
|
||||||
documentId: ID!
|
documentId: ID!
|
||||||
changelog: String
|
changelog: String
|
||||||
|
|||||||
@@ -159,6 +159,14 @@ type ComplexityRoot struct {
|
|||||||
Node func(childComplexity int) int
|
Node func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BulkDeleteDocumentsPayload struct {
|
||||||
|
DeletedDocumentIds func(childComplexity int) int
|
||||||
|
}
|
||||||
|
|
||||||
|
BulkExportDocumentsPayload struct {
|
||||||
|
ExportJobID func(childComplexity int) int
|
||||||
|
}
|
||||||
|
|
||||||
BulkPublishDocumentVersionsPayload struct {
|
BulkPublishDocumentVersionsPayload struct {
|
||||||
DocumentEdges func(childComplexity int) int
|
DocumentEdges func(childComplexity int) int
|
||||||
DocumentVersionEdges func(childComplexity int) int
|
DocumentVersionEdges func(childComplexity int) int
|
||||||
@@ -714,6 +722,8 @@ type ComplexityRoot struct {
|
|||||||
Mutation struct {
|
Mutation struct {
|
||||||
AssessVendor func(childComplexity int, input types.AssessVendorInput) int
|
AssessVendor func(childComplexity int, input types.AssessVendorInput) int
|
||||||
AssignTask func(childComplexity int, input types.AssignTaskInput) int
|
AssignTask func(childComplexity int, input types.AssignTaskInput) int
|
||||||
|
BulkDeleteDocuments func(childComplexity int, input types.BulkDeleteDocumentsInput) int
|
||||||
|
BulkExportDocuments func(childComplexity int, input types.BulkExportDocumentsInput) int
|
||||||
BulkPublishDocumentVersions func(childComplexity int, input types.BulkPublishDocumentVersionsInput) int
|
BulkPublishDocumentVersions func(childComplexity int, input types.BulkPublishDocumentVersionsInput) int
|
||||||
BulkRequestSignatures func(childComplexity int, input types.BulkRequestSignaturesInput) int
|
BulkRequestSignatures func(childComplexity int, input types.BulkRequestSignaturesInput) int
|
||||||
CancelSignatureRequest func(childComplexity int, input types.CancelSignatureRequestInput) int
|
CancelSignatureRequest func(childComplexity int, input types.CancelSignatureRequestInput) int
|
||||||
@@ -1629,6 +1639,8 @@ type MutationResolver interface {
|
|||||||
DeleteDocument(ctx context.Context, input types.DeleteDocumentInput) (*types.DeleteDocumentPayload, error)
|
DeleteDocument(ctx context.Context, input types.DeleteDocumentInput) (*types.DeleteDocumentPayload, error)
|
||||||
PublishDocumentVersion(ctx context.Context, input types.PublishDocumentVersionInput) (*types.PublishDocumentVersionPayload, error)
|
PublishDocumentVersion(ctx context.Context, input types.PublishDocumentVersionInput) (*types.PublishDocumentVersionPayload, error)
|
||||||
BulkPublishDocumentVersions(ctx context.Context, input types.BulkPublishDocumentVersionsInput) (*types.BulkPublishDocumentVersionsPayload, error)
|
BulkPublishDocumentVersions(ctx context.Context, input types.BulkPublishDocumentVersionsInput) (*types.BulkPublishDocumentVersionsPayload, error)
|
||||||
|
BulkDeleteDocuments(ctx context.Context, input types.BulkDeleteDocumentsInput) (*types.BulkDeleteDocumentsPayload, error)
|
||||||
|
BulkExportDocuments(ctx context.Context, input types.BulkExportDocumentsInput) (*types.BulkExportDocumentsPayload, error)
|
||||||
GenerateDocumentChangelog(ctx context.Context, input types.GenerateDocumentChangelogInput) (*types.GenerateDocumentChangelogPayload, error)
|
GenerateDocumentChangelog(ctx context.Context, input types.GenerateDocumentChangelogInput) (*types.GenerateDocumentChangelogPayload, error)
|
||||||
CreateDraftDocumentVersion(ctx context.Context, input types.CreateDraftDocumentVersionInput) (*types.CreateDraftDocumentVersionPayload, error)
|
CreateDraftDocumentVersion(ctx context.Context, input types.CreateDraftDocumentVersionInput) (*types.CreateDraftDocumentVersionPayload, error)
|
||||||
DeleteDraftDocumentVersion(ctx context.Context, input types.DeleteDraftDocumentVersionInput) (*types.DeleteDraftDocumentVersionPayload, error)
|
DeleteDraftDocumentVersion(ctx context.Context, input types.DeleteDraftDocumentVersionInput) (*types.DeleteDraftDocumentVersionPayload, error)
|
||||||
@@ -2090,6 +2102,20 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
|
|
||||||
return e.complexity.AuditEdge.Node(childComplexity), true
|
return e.complexity.AuditEdge.Node(childComplexity), true
|
||||||
|
|
||||||
|
case "BulkDeleteDocumentsPayload.deletedDocumentIds":
|
||||||
|
if e.complexity.BulkDeleteDocumentsPayload.DeletedDocumentIds == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.BulkDeleteDocumentsPayload.DeletedDocumentIds(childComplexity), true
|
||||||
|
|
||||||
|
case "BulkExportDocumentsPayload.exportJobId":
|
||||||
|
if e.complexity.BulkExportDocumentsPayload.ExportJobID == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.BulkExportDocumentsPayload.ExportJobID(childComplexity), true
|
||||||
|
|
||||||
case "BulkPublishDocumentVersionsPayload.documentEdges":
|
case "BulkPublishDocumentVersionsPayload.documentEdges":
|
||||||
if e.complexity.BulkPublishDocumentVersionsPayload.DocumentEdges == nil {
|
if e.complexity.BulkPublishDocumentVersionsPayload.DocumentEdges == nil {
|
||||||
break
|
break
|
||||||
@@ -3859,6 +3885,30 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
|
|
||||||
return e.complexity.Mutation.AssignTask(childComplexity, args["input"].(types.AssignTaskInput)), true
|
return e.complexity.Mutation.AssignTask(childComplexity, args["input"].(types.AssignTaskInput)), true
|
||||||
|
|
||||||
|
case "Mutation.bulkDeleteDocuments":
|
||||||
|
if e.complexity.Mutation.BulkDeleteDocuments == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
args, err := ec.field_Mutation_bulkDeleteDocuments_args(ctx, rawArgs)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.Mutation.BulkDeleteDocuments(childComplexity, args["input"].(types.BulkDeleteDocumentsInput)), true
|
||||||
|
|
||||||
|
case "Mutation.bulkExportDocuments":
|
||||||
|
if e.complexity.Mutation.BulkExportDocuments == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
args, err := ec.field_Mutation_bulkExportDocuments_args(ctx, rawArgs)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.Mutation.BulkExportDocuments(childComplexity, args["input"].(types.BulkExportDocumentsInput)), true
|
||||||
|
|
||||||
case "Mutation.bulkPublishDocumentVersions":
|
case "Mutation.bulkPublishDocumentVersions":
|
||||||
if e.complexity.Mutation.BulkPublishDocumentVersions == nil {
|
if e.complexity.Mutation.BulkPublishDocumentVersions == nil {
|
||||||
break
|
break
|
||||||
@@ -7834,6 +7884,8 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
|
|||||||
ec.unmarshalInputAssetOrder,
|
ec.unmarshalInputAssetOrder,
|
||||||
ec.unmarshalInputAssignTaskInput,
|
ec.unmarshalInputAssignTaskInput,
|
||||||
ec.unmarshalInputAuditOrder,
|
ec.unmarshalInputAuditOrder,
|
||||||
|
ec.unmarshalInputBulkDeleteDocumentsInput,
|
||||||
|
ec.unmarshalInputBulkExportDocumentsInput,
|
||||||
ec.unmarshalInputBulkPublishDocumentVersionsInput,
|
ec.unmarshalInputBulkPublishDocumentVersionsInput,
|
||||||
ec.unmarshalInputBulkRequestSignaturesInput,
|
ec.unmarshalInputBulkRequestSignaturesInput,
|
||||||
ec.unmarshalInputCancelSignatureRequestInput,
|
ec.unmarshalInputCancelSignatureRequestInput,
|
||||||
@@ -10724,6 +10776,12 @@ type Mutation {
|
|||||||
bulkPublishDocumentVersions(
|
bulkPublishDocumentVersions(
|
||||||
input: BulkPublishDocumentVersionsInput!
|
input: BulkPublishDocumentVersionsInput!
|
||||||
): BulkPublishDocumentVersionsPayload!
|
): BulkPublishDocumentVersionsPayload!
|
||||||
|
bulkDeleteDocuments(
|
||||||
|
input: BulkDeleteDocumentsInput!
|
||||||
|
): BulkDeleteDocumentsPayload!
|
||||||
|
bulkExportDocuments(
|
||||||
|
input: BulkExportDocumentsInput!
|
||||||
|
): BulkExportDocumentsPayload!
|
||||||
generateDocumentChangelog(
|
generateDocumentChangelog(
|
||||||
input: GenerateDocumentChangelogInput!
|
input: GenerateDocumentChangelogInput!
|
||||||
): GenerateDocumentChangelogPayload!
|
): GenerateDocumentChangelogPayload!
|
||||||
@@ -11937,6 +11995,22 @@ type BulkPublishDocumentVersionsPayload {
|
|||||||
documentEdges: [DocumentEdge!]!
|
documentEdges: [DocumentEdge!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input BulkDeleteDocumentsInput {
|
||||||
|
documentIds: [ID!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
input BulkExportDocumentsInput {
|
||||||
|
documentIds: [ID!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type BulkDeleteDocumentsPayload {
|
||||||
|
deletedDocumentIds: [ID!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type BulkExportDocumentsPayload {
|
||||||
|
exportJobId: ID!
|
||||||
|
}
|
||||||
|
|
||||||
input PublishDocumentVersionInput {
|
input PublishDocumentVersionInput {
|
||||||
documentId: ID!
|
documentId: ID!
|
||||||
changelog: String
|
changelog: String
|
||||||
@@ -13861,6 +13935,52 @@ func (ec *executionContext) field_Mutation_assignTask_argsInput(
|
|||||||
return zeroVal, nil
|
return zeroVal, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) field_Mutation_bulkDeleteDocuments_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
||||||
|
var err error
|
||||||
|
args := map[string]any{}
|
||||||
|
arg0, err := ec.field_Mutation_bulkDeleteDocuments_argsInput(ctx, rawArgs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args["input"] = arg0
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
func (ec *executionContext) field_Mutation_bulkDeleteDocuments_argsInput(
|
||||||
|
ctx context.Context,
|
||||||
|
rawArgs map[string]any,
|
||||||
|
) (types.BulkDeleteDocumentsInput, error) {
|
||||||
|
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
|
||||||
|
if tmp, ok := rawArgs["input"]; ok {
|
||||||
|
return ec.unmarshalNBulkDeleteDocumentsInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkDeleteDocumentsInput(ctx, tmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var zeroVal types.BulkDeleteDocumentsInput
|
||||||
|
return zeroVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) field_Mutation_bulkExportDocuments_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
||||||
|
var err error
|
||||||
|
args := map[string]any{}
|
||||||
|
arg0, err := ec.field_Mutation_bulkExportDocuments_argsInput(ctx, rawArgs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args["input"] = arg0
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
func (ec *executionContext) field_Mutation_bulkExportDocuments_argsInput(
|
||||||
|
ctx context.Context,
|
||||||
|
rawArgs map[string]any,
|
||||||
|
) (types.BulkExportDocumentsInput, error) {
|
||||||
|
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
|
||||||
|
if tmp, ok := rawArgs["input"]; ok {
|
||||||
|
return ec.unmarshalNBulkExportDocumentsInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkExportDocumentsInput(ctx, tmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var zeroVal types.BulkExportDocumentsInput
|
||||||
|
return zeroVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) field_Mutation_bulkPublishDocumentVersions_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
func (ec *executionContext) field_Mutation_bulkPublishDocumentVersions_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
||||||
var err error
|
var err error
|
||||||
args := map[string]any{}
|
args := map[string]any{}
|
||||||
@@ -21578,6 +21698,94 @@ func (ec *executionContext) fieldContext_AuditEdge_node(_ context.Context, field
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _BulkDeleteDocumentsPayload_deletedDocumentIds(ctx context.Context, field graphql.CollectedField, obj *types.BulkDeleteDocumentsPayload) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_BulkDeleteDocumentsPayload_deletedDocumentIds(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.DeletedDocumentIds, 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.([]gid.GID)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNID2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGIDᚄ(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_BulkDeleteDocumentsPayload_deletedDocumentIds(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "BulkDeleteDocumentsPayload",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type ID does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _BulkExportDocumentsPayload_exportJobId(ctx context.Context, field graphql.CollectedField, obj *types.BulkExportDocumentsPayload) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_BulkExportDocumentsPayload_exportJobId(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.ExportJobID, 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.(gid.GID)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_BulkExportDocumentsPayload_exportJobId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "BulkExportDocumentsPayload",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type ID does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _BulkPublishDocumentVersionsPayload_documentVersionEdges(ctx context.Context, field graphql.CollectedField, obj *types.BulkPublishDocumentVersionsPayload) (ret graphql.Marshaler) {
|
func (ec *executionContext) _BulkPublishDocumentVersionsPayload_documentVersionEdges(ctx context.Context, field graphql.CollectedField, obj *types.BulkPublishDocumentVersionsPayload) (ret graphql.Marshaler) {
|
||||||
fc, err := ec.fieldContext_BulkPublishDocumentVersionsPayload_documentVersionEdges(ctx, field)
|
fc, err := ec.fieldContext_BulkPublishDocumentVersionsPayload_documentVersionEdges(ctx, field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -37958,6 +38166,124 @@ func (ec *executionContext) fieldContext_Mutation_bulkPublishDocumentVersions(ct
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Mutation_bulkDeleteDocuments(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_Mutation_bulkDeleteDocuments(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().BulkDeleteDocuments(rctx, fc.Args["input"].(types.BulkDeleteDocumentsInput))
|
||||||
|
})
|
||||||
|
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.BulkDeleteDocumentsPayload)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNBulkDeleteDocumentsPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkDeleteDocumentsPayload(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_Mutation_bulkDeleteDocuments(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 "deletedDocumentIds":
|
||||||
|
return ec.fieldContext_BulkDeleteDocumentsPayload_deletedDocumentIds(ctx, field)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no field named %q was found under type BulkDeleteDocumentsPayload", 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_bulkDeleteDocuments_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return fc, err
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Mutation_bulkExportDocuments(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_Mutation_bulkExportDocuments(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().BulkExportDocuments(rctx, fc.Args["input"].(types.BulkExportDocumentsInput))
|
||||||
|
})
|
||||||
|
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.BulkExportDocumentsPayload)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNBulkExportDocumentsPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkExportDocumentsPayload(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_Mutation_bulkExportDocuments(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 "exportJobId":
|
||||||
|
return ec.fieldContext_BulkExportDocumentsPayload_exportJobId(ctx, field)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no field named %q was found under type BulkExportDocumentsPayload", 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_bulkExportDocuments_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return fc, err
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Mutation_generateDocumentChangelog(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Mutation_generateDocumentChangelog(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
fc, err := ec.fieldContext_Mutation_generateDocumentChangelog(ctx, field)
|
fc, err := ec.fieldContext_Mutation_generateDocumentChangelog(ctx, field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -61021,6 +61347,60 @@ func (ec *executionContext) unmarshalInputAuditOrder(ctx context.Context, obj an
|
|||||||
return it, nil
|
return it, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) unmarshalInputBulkDeleteDocumentsInput(ctx context.Context, obj any) (types.BulkDeleteDocumentsInput, error) {
|
||||||
|
var it types.BulkDeleteDocumentsInput
|
||||||
|
asMap := map[string]any{}
|
||||||
|
for k, v := range obj.(map[string]any) {
|
||||||
|
asMap[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldsInOrder := [...]string{"documentIds"}
|
||||||
|
for _, k := range fieldsInOrder {
|
||||||
|
v, ok := asMap[k]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch k {
|
||||||
|
case "documentIds":
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("documentIds"))
|
||||||
|
data, err := ec.unmarshalNID2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGIDᚄ(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return it, err
|
||||||
|
}
|
||||||
|
it.DocumentIds = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return it, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) unmarshalInputBulkExportDocumentsInput(ctx context.Context, obj any) (types.BulkExportDocumentsInput, error) {
|
||||||
|
var it types.BulkExportDocumentsInput
|
||||||
|
asMap := map[string]any{}
|
||||||
|
for k, v := range obj.(map[string]any) {
|
||||||
|
asMap[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldsInOrder := [...]string{"documentIds"}
|
||||||
|
for _, k := range fieldsInOrder {
|
||||||
|
v, ok := asMap[k]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch k {
|
||||||
|
case "documentIds":
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("documentIds"))
|
||||||
|
data, err := ec.unmarshalNID2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGIDᚄ(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return it, err
|
||||||
|
}
|
||||||
|
it.DocumentIds = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return it, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) unmarshalInputBulkPublishDocumentVersionsInput(ctx context.Context, obj any) (types.BulkPublishDocumentVersionsInput, error) {
|
func (ec *executionContext) unmarshalInputBulkPublishDocumentVersionsInput(ctx context.Context, obj any) (types.BulkPublishDocumentVersionsInput, error) {
|
||||||
var it types.BulkPublishDocumentVersionsInput
|
var it types.BulkPublishDocumentVersionsInput
|
||||||
asMap := map[string]any{}
|
asMap := map[string]any{}
|
||||||
@@ -68443,6 +68823,84 @@ func (ec *executionContext) _AuditEdge(ctx context.Context, sel ast.SelectionSet
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var bulkDeleteDocumentsPayloadImplementors = []string{"BulkDeleteDocumentsPayload"}
|
||||||
|
|
||||||
|
func (ec *executionContext) _BulkDeleteDocumentsPayload(ctx context.Context, sel ast.SelectionSet, obj *types.BulkDeleteDocumentsPayload) graphql.Marshaler {
|
||||||
|
fields := graphql.CollectFields(ec.OperationContext, sel, bulkDeleteDocumentsPayloadImplementors)
|
||||||
|
|
||||||
|
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("BulkDeleteDocumentsPayload")
|
||||||
|
case "deletedDocumentIds":
|
||||||
|
out.Values[i] = ec._BulkDeleteDocumentsPayload_deletedDocumentIds(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 bulkExportDocumentsPayloadImplementors = []string{"BulkExportDocumentsPayload"}
|
||||||
|
|
||||||
|
func (ec *executionContext) _BulkExportDocumentsPayload(ctx context.Context, sel ast.SelectionSet, obj *types.BulkExportDocumentsPayload) graphql.Marshaler {
|
||||||
|
fields := graphql.CollectFields(ec.OperationContext, sel, bulkExportDocumentsPayloadImplementors)
|
||||||
|
|
||||||
|
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("BulkExportDocumentsPayload")
|
||||||
|
case "exportJobId":
|
||||||
|
out.Values[i] = ec._BulkExportDocumentsPayload_exportJobId(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 bulkPublishDocumentVersionsPayloadImplementors = []string{"BulkPublishDocumentVersionsPayload"}
|
var bulkPublishDocumentVersionsPayloadImplementors = []string{"BulkPublishDocumentVersionsPayload"}
|
||||||
|
|
||||||
func (ec *executionContext) _BulkPublishDocumentVersionsPayload(ctx context.Context, sel ast.SelectionSet, obj *types.BulkPublishDocumentVersionsPayload) graphql.Marshaler {
|
func (ec *executionContext) _BulkPublishDocumentVersionsPayload(ctx context.Context, sel ast.SelectionSet, obj *types.BulkPublishDocumentVersionsPayload) graphql.Marshaler {
|
||||||
@@ -74828,6 +75286,20 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
|
|||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
out.Invalids++
|
out.Invalids++
|
||||||
}
|
}
|
||||||
|
case "bulkDeleteDocuments":
|
||||||
|
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
||||||
|
return ec._Mutation_bulkDeleteDocuments(ctx, field)
|
||||||
|
})
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
out.Invalids++
|
||||||
|
}
|
||||||
|
case "bulkExportDocuments":
|
||||||
|
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
||||||
|
return ec._Mutation_bulkExportDocuments(ctx, field)
|
||||||
|
})
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
out.Invalids++
|
||||||
|
}
|
||||||
case "generateDocumentChangelog":
|
case "generateDocumentChangelog":
|
||||||
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
||||||
return ec._Mutation_generateDocumentChangelog(ctx, field)
|
return ec._Mutation_generateDocumentChangelog(ctx, field)
|
||||||
@@ -82656,6 +83128,44 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) unmarshalNBulkDeleteDocumentsInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkDeleteDocumentsInput(ctx context.Context, v any) (types.BulkDeleteDocumentsInput, error) {
|
||||||
|
res, err := ec.unmarshalInputBulkDeleteDocumentsInput(ctx, v)
|
||||||
|
return res, graphql.ErrorOnPath(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNBulkDeleteDocumentsPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkDeleteDocumentsPayload(ctx context.Context, sel ast.SelectionSet, v types.BulkDeleteDocumentsPayload) graphql.Marshaler {
|
||||||
|
return ec._BulkDeleteDocumentsPayload(ctx, sel, &v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNBulkDeleteDocumentsPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkDeleteDocumentsPayload(ctx context.Context, sel ast.SelectionSet, v *types.BulkDeleteDocumentsPayload) 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._BulkDeleteDocumentsPayload(ctx, sel, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) unmarshalNBulkExportDocumentsInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkExportDocumentsInput(ctx context.Context, v any) (types.BulkExportDocumentsInput, error) {
|
||||||
|
res, err := ec.unmarshalInputBulkExportDocumentsInput(ctx, v)
|
||||||
|
return res, graphql.ErrorOnPath(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNBulkExportDocumentsPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkExportDocumentsPayload(ctx context.Context, sel ast.SelectionSet, v types.BulkExportDocumentsPayload) graphql.Marshaler {
|
||||||
|
return ec._BulkExportDocumentsPayload(ctx, sel, &v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNBulkExportDocumentsPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkExportDocumentsPayload(ctx context.Context, sel ast.SelectionSet, v *types.BulkExportDocumentsPayload) 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._BulkExportDocumentsPayload(ctx, sel, v)
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) unmarshalNBulkPublishDocumentVersionsInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkPublishDocumentVersionsInput(ctx context.Context, v any) (types.BulkPublishDocumentVersionsInput, error) {
|
func (ec *executionContext) unmarshalNBulkPublishDocumentVersionsInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkPublishDocumentVersionsInput(ctx context.Context, v any) (types.BulkPublishDocumentVersionsInput, error) {
|
||||||
res, err := ec.unmarshalInputBulkPublishDocumentVersionsInput(ctx, v)
|
res, err := ec.unmarshalInputBulkPublishDocumentVersionsInput(ctx, v)
|
||||||
return res, graphql.ErrorOnPath(ctx, err)
|
return res, graphql.ErrorOnPath(ctx, err)
|
||||||
|
|||||||
@@ -85,6 +85,22 @@ type AuditEdge struct {
|
|||||||
Node *Audit `json:"node"`
|
Node *Audit `json:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BulkDeleteDocumentsInput struct {
|
||||||
|
DocumentIds []gid.GID `json:"documentIds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BulkDeleteDocumentsPayload struct {
|
||||||
|
DeletedDocumentIds []gid.GID `json:"deletedDocumentIds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BulkExportDocumentsInput struct {
|
||||||
|
DocumentIds []gid.GID `json:"documentIds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BulkExportDocumentsPayload struct {
|
||||||
|
ExportJobID gid.GID `json:"exportJobId"`
|
||||||
|
}
|
||||||
|
|
||||||
type BulkPublishDocumentVersionsInput struct {
|
type BulkPublishDocumentVersionsInput struct {
|
||||||
DocumentIds []gid.GID `json:"documentIds"`
|
DocumentIds []gid.GID `json:"documentIds"`
|
||||||
Changelog string `json:"changelog"`
|
Changelog string `json:"changelog"`
|
||||||
|
|||||||
@@ -2543,6 +2543,49 @@ func (r *mutationResolver) BulkPublishDocumentVersions(ctx context.Context, inpu
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BulkDeleteDocuments is the resolver for the bulkDeleteDocuments field.
|
||||||
|
func (r *mutationResolver) BulkDeleteDocuments(ctx context.Context, input types.BulkDeleteDocumentsInput) (*types.BulkDeleteDocumentsPayload, error) {
|
||||||
|
if len(input.DocumentIds) == 0 {
|
||||||
|
return &types.BulkDeleteDocumentsPayload{
|
||||||
|
DeletedDocumentIds: []gid.GID{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
prb := r.ProboService(ctx, input.DocumentIds[0].TenantID())
|
||||||
|
|
||||||
|
err := prb.Documents.BulkSoftDelete(ctx, input.DocumentIds)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("cannot bulk delete documents: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.BulkDeleteDocumentsPayload{
|
||||||
|
DeletedDocumentIds: input.DocumentIds,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkExportDocuments is the resolver for the bulkExportDocuments field.
|
||||||
|
func (r *mutationResolver) BulkExportDocuments(ctx context.Context, input types.BulkExportDocumentsInput) (*types.BulkExportDocumentsPayload, error) {
|
||||||
|
if len(input.DocumentIds) == 0 {
|
||||||
|
panic(fmt.Errorf("no document ids provided"))
|
||||||
|
}
|
||||||
|
|
||||||
|
prb := r.ProboService(ctx, input.DocumentIds[0].TenantID())
|
||||||
|
|
||||||
|
user := UserFromContext(ctx)
|
||||||
|
if user == nil {
|
||||||
|
panic(fmt.Errorf("user not found"))
|
||||||
|
}
|
||||||
|
|
||||||
|
documentExport, err := prb.Documents.RequestExport(ctx, input.DocumentIds, user.EmailAddress, user.FullName)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("cannot request document export: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.BulkExportDocumentsPayload{
|
||||||
|
ExportJobID: documentExport.ID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GenerateDocumentChangelog is the resolver for the generateDocumentChangelog field.
|
// GenerateDocumentChangelog is the resolver for the generateDocumentChangelog field.
|
||||||
func (r *mutationResolver) GenerateDocumentChangelog(ctx context.Context, input types.GenerateDocumentChangelogInput) (*types.GenerateDocumentChangelogPayload, error) {
|
func (r *mutationResolver) GenerateDocumentChangelog(ctx context.Context, input types.GenerateDocumentChangelogInput) (*types.GenerateDocumentChangelogPayload, error) {
|
||||||
prb := r.ProboService(ctx, input.DocumentID.TenantID())
|
prb := r.ProboService(ctx, input.DocumentID.TenantID())
|
||||||
|
|||||||
Reference in New Issue
Block a user