Add vendor publish to document system
Replace the old snapshot-based system for vendors with the publish document system, mirroring the prior processing activity / DPIA / TIA migration. Includes the GraphQL mutation, MCP tool, CLI command, n8n operation, frontend publish dialog, e2e tests, and a prosemirror register template covering vendor profile fields plus per-vendor sections for services, contacts, risk assessments, compliance reports, BAA and DPA agreements. The vendor register lives as a generated DocumentTypeRegister document on the organization, reused across publishes (the major version bumps on every republish). Approvers can be passed in to create a draft pending approval; otherwise the version is published immediately. The frontend Vendors page exposes a Publish button and a Document link button when the document exists, and pre-fills the previous default approvers. Remove snapshot mode entirely from vendors and their sub-entities: drop snapshotId/sourceId from GraphQL Vendor type and VendorFilter; remove SnapshotsTypeVendors from the snapshot registry and delete Vendors.Snapshot, VendorSnapshotter interface and all *.InsertVendorSnapshots methods on contacts, services, risk assessments, compliance reports, BAA and DPA. Drop the snapshot routes and banner from the frontend. The snapshot_id columns remain in the database but are now filtered out with snapshot_id IS NULL. Add Get/Upsert/Clear GeneratedDocumentID methods on Vendor backed by a new vendors_document_id column on generated_documents, matching the ProcessingActivity/Finding/Obligation pattern. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -109,12 +109,21 @@ export const useDeleteVendor = (
|
|||||||
export const vendorConnectionKey = "VendorsPage_vendors";
|
export const vendorConnectionKey = "VendorsPage_vendors";
|
||||||
|
|
||||||
export const vendorsQuery = graphql`
|
export const vendorsQuery = graphql`
|
||||||
query VendorGraphListQuery($organizationId: ID!, $snapshotId: ID) {
|
query VendorGraphListQuery($organizationId: ID!) {
|
||||||
node(id: $organizationId) {
|
node(id: $organizationId) {
|
||||||
... on Organization {
|
... on Organization {
|
||||||
id
|
id
|
||||||
canCreateVendor: permission(action: "core:vendor:create")
|
canCreateVendor: permission(action: "core:vendor:create")
|
||||||
...VendorGraphPaginatedFragment @arguments(snapshotId: $snapshotId)
|
canPublishVendor: permission(action: "core:vendor:publish")
|
||||||
|
vendorsDocument {
|
||||||
|
id
|
||||||
|
currentPublishedMajor
|
||||||
|
currentPublishedMinor
|
||||||
|
defaultApprovers {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
...VendorGraphPaginatedFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,7 +138,6 @@ export const paginatedVendorsFragment = graphql`
|
|||||||
after: { type: "CursorKey", defaultValue: null }
|
after: { type: "CursorKey", defaultValue: null }
|
||||||
before: { type: "CursorKey", defaultValue: null }
|
before: { type: "CursorKey", defaultValue: null }
|
||||||
last: { type: "Int", defaultValue: null }
|
last: { type: "Int", defaultValue: null }
|
||||||
snapshotId: { type: "ID", defaultValue: null }
|
|
||||||
) {
|
) {
|
||||||
vendors(
|
vendors(
|
||||||
first: $first
|
first: $first
|
||||||
@@ -137,13 +145,11 @@ export const paginatedVendorsFragment = graphql`
|
|||||||
last: $last
|
last: $last
|
||||||
before: $before
|
before: $before
|
||||||
orderBy: $order
|
orderBy: $order
|
||||||
filter: { snapshotId: $snapshotId }
|
) @connection(key: "VendorsListQuery_vendors") {
|
||||||
) @connection(key: "VendorsListQuery_vendors", filters: ["filter"]) {
|
|
||||||
__id
|
__id
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
snapshotId
|
|
||||||
name
|
name
|
||||||
websiteUrl
|
websiteUrl
|
||||||
updatedAt
|
updatedAt
|
||||||
@@ -174,7 +180,6 @@ export const vendorNodeQuery = graphql`
|
|||||||
node(id: $vendorId) {
|
node(id: $vendorId) {
|
||||||
id
|
id
|
||||||
... on Vendor {
|
... on Vendor {
|
||||||
snapshotId
|
|
||||||
name
|
name
|
||||||
websiteUrl
|
websiteUrl
|
||||||
canAssess: permission(action: "core:vendor:assess")
|
canAssess: permission(action: "core:vendor:assess")
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
import { faviconUrl, validateSnapshotConsistency } from "@probo/helpers";
|
import { faviconUrl } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import {
|
import {
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
@@ -31,11 +31,10 @@ import {
|
|||||||
useFragment,
|
useFragment,
|
||||||
usePreloadedQuery,
|
usePreloadedQuery,
|
||||||
} from "react-relay";
|
} from "react-relay";
|
||||||
import { Outlet, useParams } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
|
|
||||||
import type { VendorComplianceTabFragment$key } from "#/__generated__/core/VendorComplianceTabFragment.graphql";
|
import type { VendorComplianceTabFragment$key } from "#/__generated__/core/VendorComplianceTabFragment.graphql";
|
||||||
import type { VendorGraphNodeQuery } from "#/__generated__/core/VendorGraphNodeQuery.graphql";
|
import type { VendorGraphNodeQuery } from "#/__generated__/core/VendorGraphNodeQuery.graphql";
|
||||||
import { SnapshotBanner } from "#/components/SnapshotBanner";
|
|
||||||
import {
|
import {
|
||||||
useDeleteVendor,
|
useDeleteVendor,
|
||||||
vendorConnectionKey,
|
vendorConnectionKey,
|
||||||
@@ -54,10 +53,7 @@ export default function VendorDetailPage(props: Props) {
|
|||||||
const { node: vendor } = usePreloadedQuery(vendorNodeQuery, props.queryRef);
|
const { node: vendor } = usePreloadedQuery(vendorNodeQuery, props.queryRef);
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
|
||||||
const isSnapshotMode = Boolean(snapshotId);
|
|
||||||
|
|
||||||
validateSnapshotConsistency(vendor, snapshotId);
|
|
||||||
const deleteVendor = useDeleteVendor(
|
const deleteVendor = useDeleteVendor(
|
||||||
vendor,
|
vendor,
|
||||||
ConnectionHandler.getConnectionID(organizationId, vendorConnectionKey),
|
ConnectionHandler.getConnectionID(organizationId, vendorConnectionKey),
|
||||||
@@ -68,19 +64,13 @@ export default function VendorDetailPage(props: Props) {
|
|||||||
vendor as VendorComplianceTabFragment$key,
|
vendor as VendorComplianceTabFragment$key,
|
||||||
).complianceReports.edges.length;
|
).complianceReports.edges.length;
|
||||||
|
|
||||||
const vendorsUrl
|
const vendorsUrl = `/organizations/${organizationId}/vendors`;
|
||||||
= isSnapshotMode && snapshotId
|
|
||||||
? `/organizations/${organizationId}/snapshots/${snapshotId}/vendors`
|
|
||||||
: `/organizations/${organizationId}/vendors`;
|
|
||||||
|
|
||||||
const baseVendorUrl
|
const baseVendorUrl
|
||||||
= isSnapshotMode && snapshotId
|
= `/organizations/${organizationId}/vendors/${vendor.id}`;
|
||||||
? `/organizations/${organizationId}/snapshots/${snapshotId}/vendors/${vendor.id}`
|
|
||||||
: `/organizations/${organizationId}/vendors/${vendor.id}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
|
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
@@ -103,7 +93,6 @@ export default function VendorDetailPage(props: Props) {
|
|||||||
)}
|
)}
|
||||||
<div className="text-2xl">{vendor.name}</div>
|
<div className="text-2xl">{vendor.name}</div>
|
||||||
</div>
|
</div>
|
||||||
{!isSnapshotMode && (
|
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
{vendor.canAssess && (
|
{vendor.canAssess && (
|
||||||
<ImportAssessmentDialog vendorId={vendor.id}>
|
<ImportAssessmentDialog vendorId={vendor.id}>
|
||||||
@@ -124,7 +113,6 @@ export default function VendorDetailPage(props: Props) {
|
|||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
|
IconPageTextLine,
|
||||||
IconPlusLarge,
|
IconPlusLarge,
|
||||||
IconTrashCan,
|
IconTrashCan,
|
||||||
|
IconUpload,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
RiskBadge,
|
RiskBadge,
|
||||||
Tbody,
|
Tbody,
|
||||||
@@ -35,14 +37,13 @@ import {
|
|||||||
usePaginationFragment,
|
usePaginationFragment,
|
||||||
usePreloadedQuery,
|
usePreloadedQuery,
|
||||||
} from "react-relay";
|
} from "react-relay";
|
||||||
import { useParams } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
import type { VendorGraphListQuery } from "#/__generated__/core/VendorGraphListQuery.graphql";
|
import type { VendorGraphListQuery } from "#/__generated__/core/VendorGraphListQuery.graphql";
|
||||||
import type {
|
import type {
|
||||||
VendorGraphPaginatedFragment$data,
|
VendorGraphPaginatedFragment$data,
|
||||||
VendorGraphPaginatedFragment$key,
|
VendorGraphPaginatedFragment$key,
|
||||||
} from "#/__generated__/core/VendorGraphPaginatedFragment.graphql";
|
} from "#/__generated__/core/VendorGraphPaginatedFragment.graphql";
|
||||||
import { SnapshotBanner } from "#/components/SnapshotBanner";
|
|
||||||
import { SortableTable, SortableTh } from "#/components/SortableTable";
|
import { SortableTable, SortableTh } from "#/components/SortableTable";
|
||||||
import {
|
import {
|
||||||
paginatedVendorsFragment,
|
paginatedVendorsFragment,
|
||||||
@@ -53,6 +54,7 @@ import { useOrganizationId } from "#/hooks/useOrganizationId";
|
|||||||
import type { NodeOf } from "#/types";
|
import type { NodeOf } from "#/types";
|
||||||
|
|
||||||
import { CreateVendorDialog } from "./dialogs/CreateVendorDialog";
|
import { CreateVendorDialog } from "./dialogs/CreateVendorDialog";
|
||||||
|
import { PublishVendorListDialog } from "./dialogs/PublishVendorListDialog";
|
||||||
|
|
||||||
type Vendor = NodeOf<VendorGraphPaginatedFragment$data["vendors"]>;
|
type Vendor = NodeOf<VendorGraphPaginatedFragment$data["vendors"]>;
|
||||||
|
|
||||||
@@ -63,8 +65,7 @@ type Props = {
|
|||||||
export default function VendorsPage(props: Props) {
|
export default function VendorsPage(props: Props) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
const navigate = useNavigate();
|
||||||
const isSnapshotMode = Boolean(snapshotId);
|
|
||||||
|
|
||||||
const data = usePreloadedQuery(vendorsQuery, props.queryRef);
|
const data = usePreloadedQuery(vendorsQuery, props.queryRef);
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
@@ -79,19 +80,46 @@ export default function VendorsPage(props: Props) {
|
|||||||
usePageTitle(__("Vendors"));
|
usePageTitle(__("Vendors"));
|
||||||
|
|
||||||
const hasAnyAction
|
const hasAnyAction
|
||||||
= !isSnapshotMode
|
= vendors.some(({ canUpdate, canDelete }) => canUpdate || canDelete);
|
||||||
&& vendors.some(({ canUpdate, canDelete }) => canUpdate || canDelete);
|
|
||||||
|
const vendorsDocument = data.node?.vendorsDocument;
|
||||||
|
const defaultApproverIds
|
||||||
|
= vendorsDocument?.defaultApprovers?.map(a => a.id) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={__("Vendors")}
|
title={__("Vendors")}
|
||||||
description={__(
|
description={__(
|
||||||
"Vendors are third-party services that your company uses. Add them to keep track of their risk and compliance status.",
|
"Vendors are third-party services that your company uses. Add them to keep track of their risk and compliance status.",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!isSnapshotMode && data.node.canCreateVendor && (
|
<div className="flex gap-2">
|
||||||
|
{vendorsDocument && (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
icon={IconPageTextLine}
|
||||||
|
onClick={() => void navigate(
|
||||||
|
`/organizations/${organizationId}/documents/${vendorsDocument.id}`,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{__("Document")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{data.node.canPublishVendor && (
|
||||||
|
<PublishVendorListDialog
|
||||||
|
organizationId={organizationId}
|
||||||
|
defaultApproverIds={defaultApproverIds}
|
||||||
|
onPublished={documentId => void navigate(
|
||||||
|
`/organizations/${organizationId}/documents/${documentId}`,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button variant="secondary" icon={IconUpload}>
|
||||||
|
{__("Publish")}
|
||||||
|
</Button>
|
||||||
|
</PublishVendorListDialog>
|
||||||
|
)}
|
||||||
|
{data.node.canCreateVendor && (
|
||||||
<CreateVendorDialog
|
<CreateVendorDialog
|
||||||
connection={connectionId}
|
connection={connectionId}
|
||||||
organizationId={organizationId}
|
organizationId={organizationId}
|
||||||
@@ -99,6 +127,7 @@ export default function VendorsPage(props: Props) {
|
|||||||
<Button icon={IconPlusLarge}>{__("Add vendor")}</Button>
|
<Button icon={IconPlusLarge}>{__("Add vendor")}</Button>
|
||||||
</CreateVendorDialog>
|
</CreateVendorDialog>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<SortableTable {...pagination}>
|
<SortableTable {...pagination}>
|
||||||
<Thead>
|
<Thead>
|
||||||
@@ -137,16 +166,11 @@ function VendorRow({
|
|||||||
connectionId: string;
|
connectionId: string;
|
||||||
hasAnyAction: boolean;
|
hasAnyAction: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
|
||||||
const isSnapshotMode = Boolean(snapshotId);
|
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const latestAssessment = vendor.riskAssessments?.edges[0]?.node;
|
const latestAssessment = vendor.riskAssessments?.edges[0]?.node;
|
||||||
const deleteVendor = useDeleteVendor(vendor, connectionId);
|
const deleteVendor = useDeleteVendor(vendor, connectionId);
|
||||||
|
|
||||||
const vendorUrl
|
const vendorUrl = `/organizations/${organizationId}/vendors/${vendor.id}/overview`;
|
||||||
= isSnapshotMode && snapshotId
|
|
||||||
? `/organizations/${organizationId}/snapshots/${snapshotId}/vendors/${vendor.id}/overview`
|
|
||||||
: `/organizations/${organizationId}/vendors/${vendor.id}/overview`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
160
apps/console/src/pages/organizations/vendors/dialogs/PublishVendorListDialog.tsx
vendored
Normal file
160
apps/console/src/pages/organizations/vendors/dialogs/PublishVendorListDialog.tsx
vendored
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { formatError, type GraphQLError } from "@probo/helpers";
|
||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
IconSend,
|
||||||
|
IconUpload,
|
||||||
|
useDialogRef,
|
||||||
|
useToast,
|
||||||
|
} from "@probo/ui";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useMutation } from "react-relay";
|
||||||
|
import { graphql } from "relay-runtime";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import type { PublishVendorListDialogMutation } from "#/__generated__/core/PublishVendorListDialogMutation.graphql";
|
||||||
|
import { PeopleMultiSelectField } from "#/components/form/PeopleMultiSelectField";
|
||||||
|
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||||
|
|
||||||
|
const publishMutation = graphql`
|
||||||
|
mutation PublishVendorListDialogMutation(
|
||||||
|
$input: PublishVendorListInput!
|
||||||
|
) {
|
||||||
|
publishVendorList(input: $input) {
|
||||||
|
documentEdge {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
organizationId: string;
|
||||||
|
defaultApproverIds?: string[];
|
||||||
|
onPublished?: (documentId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PublishVendorListDialog({
|
||||||
|
children,
|
||||||
|
organizationId,
|
||||||
|
defaultApproverIds,
|
||||||
|
onPublished,
|
||||||
|
}: Props) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const dialogRef = useDialogRef();
|
||||||
|
|
||||||
|
const schema = useMemo(() =>
|
||||||
|
z.object({
|
||||||
|
approverIds: z.array(z.string()),
|
||||||
|
}), []);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
watch,
|
||||||
|
} = useFormWithSchema(schema, {
|
||||||
|
defaultValues: {
|
||||||
|
approverIds: defaultApproverIds ?? [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [publish, isPublishing]
|
||||||
|
= useMutation<PublishVendorListDialogMutation>(publishMutation);
|
||||||
|
|
||||||
|
const approverIds = watch("approverIds");
|
||||||
|
const hasApprovers = approverIds.length > 0;
|
||||||
|
|
||||||
|
const onSubmit = (data: z.infer<typeof schema>) => {
|
||||||
|
publish({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId,
|
||||||
|
approverIds: data.approverIds.length > 0 ? data.approverIds : undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted(response) {
|
||||||
|
const documentId = response.publishVendorList?.documentEdge?.node?.id;
|
||||||
|
if (documentId) {
|
||||||
|
toast({
|
||||||
|
title: __("Success"),
|
||||||
|
description: hasApprovers
|
||||||
|
? __("Approval requested successfully.")
|
||||||
|
: __("Vendors published successfully."),
|
||||||
|
variant: "success",
|
||||||
|
});
|
||||||
|
dialogRef.current?.close();
|
||||||
|
reset();
|
||||||
|
onPublished?.(documentId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
toast({
|
||||||
|
title: __("Error"),
|
||||||
|
description: formatError(
|
||||||
|
__("Failed to publish vendors"),
|
||||||
|
error as GraphQLError,
|
||||||
|
),
|
||||||
|
variant: "error",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
className="max-w-xl"
|
||||||
|
ref={dialogRef}
|
||||||
|
trigger={children}
|
||||||
|
title={__("Publish Vendors")}
|
||||||
|
>
|
||||||
|
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
||||||
|
<DialogContent padded>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-txt-secondary">
|
||||||
|
{__("Select approvers to request approval before publishing, or publish directly without approvers.")}
|
||||||
|
</p>
|
||||||
|
<PeopleMultiSelectField
|
||||||
|
name="approverIds"
|
||||||
|
label={__("Approvers")}
|
||||||
|
control={control}
|
||||||
|
organizationId={organizationId}
|
||||||
|
placeholder={__("Add approvers...")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
icon={hasApprovers ? IconSend : IconUpload}
|
||||||
|
disabled={isPublishing}
|
||||||
|
>
|
||||||
|
{hasApprovers ? __("Request approval") : __("Publish")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -34,20 +34,6 @@ export const vendorRoutes = [
|
|||||||
loader: loaderFromQueryLoader(({ organizationId }) =>
|
loader: loaderFromQueryLoader(({ organizationId }) =>
|
||||||
loadQuery<VendorGraphListQuery>(coreEnvironment, vendorsQuery, {
|
loadQuery<VendorGraphListQuery>(coreEnvironment, vendorsQuery, {
|
||||||
organizationId: organizationId,
|
organizationId: organizationId,
|
||||||
snapshotId: null,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
Component: withQueryRef(
|
|
||||||
lazy(() => import("#/pages/organizations/vendors/VendorsPage")),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "snapshots/:snapshotId/vendors",
|
|
||||||
Fallback: PageSkeleton,
|
|
||||||
loader: loaderFromQueryLoader(({ organizationId, snapshotId }) =>
|
|
||||||
loadQuery<VendorGraphListQuery>(coreEnvironment, vendorsQuery, {
|
|
||||||
organizationId: organizationId,
|
|
||||||
snapshotId,
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Component: withQueryRef(
|
Component: withQueryRef(
|
||||||
@@ -113,63 +99,4 @@ export const vendorRoutes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "snapshots/:snapshotId/vendors/:vendorId",
|
|
||||||
Fallback: PageSkeleton,
|
|
||||||
loader: loaderFromQueryLoader(({ vendorId }) =>
|
|
||||||
loadQuery<VendorGraphNodeQuery>(coreEnvironment, vendorNodeQuery, {
|
|
||||||
vendorId: vendorId,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
Component: withQueryRef(
|
|
||||||
lazy(() => import("../pages/organizations/vendors/VendorDetailPage")),
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "overview",
|
|
||||||
Fallback: LinkCardSkeleton,
|
|
||||||
Component: lazy(
|
|
||||||
() => import("../pages/organizations/vendors/tabs/VendorOverviewTab"),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "certifications",
|
|
||||||
Fallback: LinkCardSkeleton,
|
|
||||||
Component: lazy(
|
|
||||||
() =>
|
|
||||||
import("../pages/organizations/vendors/tabs/VendorCertificationsTab"),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "compliance",
|
|
||||||
Fallback: LinkCardSkeleton,
|
|
||||||
Component: lazy(
|
|
||||||
() =>
|
|
||||||
import("../pages/organizations/vendors/tabs/VendorComplianceTab"),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "risks",
|
|
||||||
Fallback: LinkCardSkeleton,
|
|
||||||
Component: lazy(
|
|
||||||
() =>
|
|
||||||
import("../pages/organizations/vendors/tabs/VendorRiskAssessmentTab"),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "contacts",
|
|
||||||
Fallback: LinkCardSkeleton,
|
|
||||||
Component: lazy(
|
|
||||||
() => import("../pages/organizations/vendors/tabs/VendorContactsTab"),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "services",
|
|
||||||
Fallback: LinkCardSkeleton,
|
|
||||||
Component: lazy(
|
|
||||||
() => import("../pages/organizations/vendors/tabs/VendorServicesTab"),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
] satisfies AppRoute[];
|
] satisfies AppRoute[];
|
||||||
|
|||||||
749
cmd/migrate-vendor-snapshots-to-documents/main.go
Normal file
749
cmd/migrate-vendor-snapshots-to-documents/main.go
Normal file
@@ -0,0 +1,749 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
// Command migrate-vendor-snapshots-to-documents creates documents and document
|
||||||
|
// versions from existing vendor snapshots. For each organization that has vendor
|
||||||
|
// snapshots, it generates a vendor list document using the same ProseMirror
|
||||||
|
// builder as the publish flow.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
"go.probo.inc/probo/pkg/docgen"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/probo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
var (
|
||||||
|
pgDSN string
|
||||||
|
dryRun bool
|
||||||
|
)
|
||||||
|
|
||||||
|
flag.StringVar(
|
||||||
|
&pgDSN,
|
||||||
|
"pg-dsn",
|
||||||
|
os.Getenv("DATABASE_URL"),
|
||||||
|
"PostgreSQL connection URL (default: DATABASE_URL env)",
|
||||||
|
)
|
||||||
|
flag.BoolVar(&dryRun, "dry-run", false, "show what would be done without writing")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if pgDSN == "" {
|
||||||
|
return fmt.Errorf("set -pg-dsn or DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
pgClient, err := newPgClientFromDSN(pgDSN)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create pg client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return migrate(ctx, pgClient, dryRun)
|
||||||
|
}
|
||||||
|
|
||||||
|
type orgWithVendorSnapshots struct {
|
||||||
|
organizationID gid.GID
|
||||||
|
tenantID gid.TenantID
|
||||||
|
organizationName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type vendorSnapshot struct {
|
||||||
|
snapshotID string
|
||||||
|
publishedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrate(ctx context.Context, pgClient *pg.Client, dryRun bool) error {
|
||||||
|
var orgs []orgWithVendorSnapshots
|
||||||
|
err := pgClient.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
||||||
|
var err error
|
||||||
|
orgs, err = loadOrgsWithVendorSnapshots(ctx, conn)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(orgs) == 0 {
|
||||||
|
fmt.Println("no organizations with vendor snapshots to migrate")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats struct {
|
||||||
|
documents, versions, failed int
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, org := range orgs {
|
||||||
|
if dryRun {
|
||||||
|
var count int
|
||||||
|
err := pgClient.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
||||||
|
snapshots, err := loadVendorSnapshots(ctx, conn, org.organizationID)
|
||||||
|
count = len(snapshots)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("would migrate org %s (%s) — %d vendor snapshot(s)\n",
|
||||||
|
org.organizationID, org.organizationName, count)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := pgClient.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
||||||
|
return migrateOrg(ctx, tx, org)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "FAIL org %s (%s): %v\n",
|
||||||
|
org.organizationID, org.organizationName, err)
|
||||||
|
stats.failed++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.documents++
|
||||||
|
}
|
||||||
|
|
||||||
|
if dryRun {
|
||||||
|
fmt.Printf("\n%d organization(s) would be migrated\n", len(orgs))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nmigrated %d organization(s), %d failed\n",
|
||||||
|
stats.documents, stats.failed)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateOrg(ctx context.Context, tx pg.Tx, org orgWithVendorSnapshots) error {
|
||||||
|
snapshots, err := loadVendorSnapshots(ctx, tx, org.organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(snapshots) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
documentID := gid.New(org.tenantID, coredata.DocumentEntityType)
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
_, err = tx.Exec(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
INSERT INTO documents (
|
||||||
|
id, tenant_id, organization_id, write_mode,
|
||||||
|
current_published_major, current_published_minor,
|
||||||
|
trust_center_visibility, status, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
@id, @tenant_id, @organization_id,
|
||||||
|
'GENERATED'::document_write_mode,
|
||||||
|
@current_published_major, 0,
|
||||||
|
'NONE'::trust_center_visibility,
|
||||||
|
'ACTIVE'::document_status,
|
||||||
|
@created_at, @updated_at
|
||||||
|
)`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"id": documentID,
|
||||||
|
"tenant_id": org.tenantID,
|
||||||
|
"organization_id": org.organizationID,
|
||||||
|
"current_published_major": len(snapshots),
|
||||||
|
"created_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot insert document: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO generated_documents (organization_id, tenant_id, vendors_document_id, created_at, updated_at)
|
||||||
|
VALUES (@organization_id, @tenant_id, @vendors_document_id, @created_at, @updated_at)
|
||||||
|
ON CONFLICT (organization_id) DO UPDATE SET vendors_document_id = @vendors_document_id, updated_at = @updated_at`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"organization_id": org.organizationID,
|
||||||
|
"tenant_id": org.tenantID,
|
||||||
|
"vendors_document_id": documentID,
|
||||||
|
"created_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot link document: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for major, snap := range snapshots {
|
||||||
|
content, err := buildSnapshotContent(ctx, tx, snap.snapshotID, org.organizationName, snap.publishedAt)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot build content for snapshot %s: %w", snap.snapshotID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
versionID := gid.New(org.tenantID, coredata.DocumentVersionEntityType)
|
||||||
|
|
||||||
|
_, err = tx.Exec(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
INSERT INTO document_versions (
|
||||||
|
id, tenant_id, organization_id, document_id,
|
||||||
|
title, major, minor, classification, document_type,
|
||||||
|
content, changelog, status, orientation,
|
||||||
|
pdf_attempt_count,
|
||||||
|
published_at, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
@id, @tenant_id, @organization_id, @document_id,
|
||||||
|
@title, @major, 0,
|
||||||
|
'CONFIDENTIAL'::document_classification,
|
||||||
|
'REGISTER'::document_type,
|
||||||
|
@content, '',
|
||||||
|
'PUBLISHED'::document_version_status,
|
||||||
|
'PORTRAIT'::document_version_orientation,
|
||||||
|
0,
|
||||||
|
@published_at, @published_at, @published_at
|
||||||
|
)`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"id": versionID,
|
||||||
|
"tenant_id": org.tenantID,
|
||||||
|
"organization_id": org.organizationID,
|
||||||
|
"document_id": documentID,
|
||||||
|
"title": "Vendors",
|
||||||
|
"major": major + 1,
|
||||||
|
"content": content,
|
||||||
|
"published_at": snap.publishedAt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot insert version for snapshot %s: %w", snap.snapshotID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("OK org %s (%s) — %d version(s)\n",
|
||||||
|
org.organizationID, org.organizationName, len(snapshots))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadOrgsWithVendorSnapshots(ctx context.Context, conn pg.Querier) ([]orgWithVendorSnapshots, error) {
|
||||||
|
rows, err := conn.Query(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
SELECT DISTINCT
|
||||||
|
o.id,
|
||||||
|
o.tenant_id,
|
||||||
|
o.name,
|
||||||
|
o.created_at
|
||||||
|
FROM organizations o
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM generated_documents gd
|
||||||
|
WHERE gd.organization_id = o.id AND gd.vendors_document_id IS NOT NULL
|
||||||
|
)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM snapshots s
|
||||||
|
WHERE s.organization_id = o.id AND s.type = 'VENDORS'
|
||||||
|
)
|
||||||
|
ORDER BY o.created_at;
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot query organizations with vendor snapshots: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var result []orgWithVendorSnapshots
|
||||||
|
for rows.Next() {
|
||||||
|
var o orgWithVendorSnapshots
|
||||||
|
var createdAt time.Time
|
||||||
|
if err := rows.Scan(&o.organizationID, &o.tenantID, &o.organizationName, &createdAt); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot scan organization: %w", err)
|
||||||
|
}
|
||||||
|
result = append(result, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadVendorSnapshots(ctx context.Context, conn pg.Querier, organizationID gid.GID) ([]vendorSnapshot, error) {
|
||||||
|
rows, err := conn.Query(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
SELECT DISTINCT
|
||||||
|
s.id,
|
||||||
|
s.created_at
|
||||||
|
FROM snapshots s
|
||||||
|
WHERE s.organization_id = @organization_id
|
||||||
|
AND s.type = 'VENDORS'
|
||||||
|
ORDER BY s.created_at ASC;
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{"organization_id": organizationID},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot query vendor snapshots for org %s: %w", organizationID, err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var result []vendorSnapshot
|
||||||
|
for rows.Next() {
|
||||||
|
var s vendorSnapshot
|
||||||
|
if err := rows.Scan(&s.snapshotID, &s.publishedAt); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot scan snapshot: %w", err)
|
||||||
|
}
|
||||||
|
result = append(result, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
type vendorInfo struct {
|
||||||
|
id string
|
||||||
|
name string
|
||||||
|
category string
|
||||||
|
|
||||||
|
legalName *string
|
||||||
|
description *string
|
||||||
|
headquarterAddress *string
|
||||||
|
websiteURL *string
|
||||||
|
privacyPolicyURL *string
|
||||||
|
serviceLevelAgreementURL *string
|
||||||
|
dataProcessingAgreementURL *string
|
||||||
|
businessAssociateAgreementURL *string
|
||||||
|
subprocessorsListURL *string
|
||||||
|
statusPageURL *string
|
||||||
|
termsOfServiceURL *string
|
||||||
|
securityPageURL *string
|
||||||
|
trustPageURL *string
|
||||||
|
certifications []string
|
||||||
|
countries []string
|
||||||
|
businessOwnerName string
|
||||||
|
securityOwnerName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSnapshotContent(
|
||||||
|
ctx context.Context,
|
||||||
|
tx pg.Tx,
|
||||||
|
snapshotID string,
|
||||||
|
orgName string,
|
||||||
|
publishedAt time.Time,
|
||||||
|
) (string, error) {
|
||||||
|
vendorRows, err := tx.Query(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
v.id,
|
||||||
|
v.name,
|
||||||
|
v.category,
|
||||||
|
v.legal_name,
|
||||||
|
v.description,
|
||||||
|
v.headquarter_address,
|
||||||
|
v.website_url,
|
||||||
|
v.privacy_policy_url,
|
||||||
|
v.service_level_agreement_url,
|
||||||
|
v.data_processing_agreement_url,
|
||||||
|
v.business_associate_agreement_url,
|
||||||
|
v.subprocessors_list_url,
|
||||||
|
v.status_page_url,
|
||||||
|
v.terms_of_service_url,
|
||||||
|
v.security_page_url,
|
||||||
|
v.trust_page_url,
|
||||||
|
v.certifications,
|
||||||
|
v.countries,
|
||||||
|
COALESCE(bo.full_name, 'Not assigned'),
|
||||||
|
COALESCE(so.full_name, 'Not assigned')
|
||||||
|
FROM vendors v
|
||||||
|
LEFT JOIN iam_membership_profiles bo ON bo.id = v.business_owner_profile_id
|
||||||
|
LEFT JOIN iam_membership_profiles so ON so.id = v.security_owner_profile_id
|
||||||
|
WHERE v.snapshot_id = @snapshot_id
|
||||||
|
ORDER BY v.name ASC;
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{"snapshot_id": snapshotID},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("cannot load snapshot vendors: %w", err)
|
||||||
|
}
|
||||||
|
defer vendorRows.Close()
|
||||||
|
|
||||||
|
var vendors []vendorInfo
|
||||||
|
for vendorRows.Next() {
|
||||||
|
var v vendorInfo
|
||||||
|
if err := vendorRows.Scan(
|
||||||
|
&v.id, &v.name, &v.category,
|
||||||
|
&v.legalName, &v.description, &v.headquarterAddress,
|
||||||
|
&v.websiteURL, &v.privacyPolicyURL, &v.serviceLevelAgreementURL,
|
||||||
|
&v.dataProcessingAgreementURL, &v.businessAssociateAgreementURL,
|
||||||
|
&v.subprocessorsListURL, &v.statusPageURL, &v.termsOfServiceURL,
|
||||||
|
&v.securityPageURL, &v.trustPageURL,
|
||||||
|
&v.certifications, &v.countries,
|
||||||
|
&v.businessOwnerName, &v.securityOwnerName,
|
||||||
|
); err != nil {
|
||||||
|
return "", fmt.Errorf("cannot scan vendor: %w", err)
|
||||||
|
}
|
||||||
|
vendors = append(vendors, v)
|
||||||
|
}
|
||||||
|
if err := vendorRows.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
vendorIDs := make([]string, len(vendors))
|
||||||
|
for i, v := range vendors {
|
||||||
|
vendorIDs[i] = v.id
|
||||||
|
}
|
||||||
|
|
||||||
|
servicesByVendor, err := loadSnapshotServices(ctx, tx, snapshotID, vendorIDs)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
contactsByVendor, err := loadSnapshotContacts(ctx, tx, snapshotID, vendorIDs)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
assessmentsByVendor, err := loadSnapshotRiskAssessments(ctx, tx, snapshotID, vendorIDs)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
reportsByVendor, err := loadSnapshotComplianceReports(ctx, tx, snapshotID, vendorIDs)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
baaByVendor, err := loadSnapshotBAAs(ctx, tx, snapshotID, vendorIDs)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
dpaByVendor, err := loadSnapshotDPAs(ctx, tx, snapshotID, vendorIDs)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]docgen.VendorListRow, 0, len(vendors))
|
||||||
|
for _, v := range vendors {
|
||||||
|
row := docgen.VendorListRow{
|
||||||
|
Name: v.name,
|
||||||
|
LegalName: deref(v.legalName),
|
||||||
|
Description: deref(v.description),
|
||||||
|
Category: formatCategory(v.category),
|
||||||
|
HeadquarterAddress: deref(v.headquarterAddress),
|
||||||
|
WebsiteURL: deref(v.websiteURL),
|
||||||
|
PrivacyPolicyURL: deref(v.privacyPolicyURL),
|
||||||
|
ServiceLevelAgreementURL: deref(v.serviceLevelAgreementURL),
|
||||||
|
DataProcessingAgreementURL: deref(v.dataProcessingAgreementURL),
|
||||||
|
BusinessAssociateAgreementURL: deref(v.businessAssociateAgreementURL),
|
||||||
|
SubprocessorsListURL: deref(v.subprocessorsListURL),
|
||||||
|
StatusPageURL: deref(v.statusPageURL),
|
||||||
|
TermsOfServiceURL: deref(v.termsOfServiceURL),
|
||||||
|
SecurityPageURL: deref(v.securityPageURL),
|
||||||
|
TrustPageURL: deref(v.trustPageURL),
|
||||||
|
Certifications: joinOrDefault(v.certifications),
|
||||||
|
Countries: joinOrDefault(v.countries),
|
||||||
|
BusinessOwner: v.businessOwnerName,
|
||||||
|
SecurityOwner: v.securityOwnerName,
|
||||||
|
Services: servicesByVendor[v.id],
|
||||||
|
Contacts: contactsByVendor[v.id],
|
||||||
|
RiskAssessments: assessmentsByVendor[v.id],
|
||||||
|
ComplianceReports: reportsByVendor[v.id],
|
||||||
|
BusinessAssociateAgreement: baaByVendor[v.id],
|
||||||
|
DataPrivacyAgreement: dpaByVendor[v.id],
|
||||||
|
}
|
||||||
|
rows = append(rows, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
docData := docgen.VendorListData{
|
||||||
|
Title: "Vendors",
|
||||||
|
OrganizationName: orgName,
|
||||||
|
CreatedAt: publishedAt,
|
||||||
|
TotalVendors: len(rows),
|
||||||
|
Rows: rows,
|
||||||
|
}
|
||||||
|
|
||||||
|
return probo.BuildVendorListDocument(docData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSnapshotServices(ctx context.Context, tx pg.Tx, snapshotID string, vendorIDs []string) (map[string][]docgen.VendorListService, error) {
|
||||||
|
rows, err := tx.Query(ctx,
|
||||||
|
`SELECT vs.vendor_id, vs.name, COALESCE(vs.description, 'Not specified')
|
||||||
|
FROM vendor_services vs
|
||||||
|
WHERE vs.snapshot_id = @snapshot_id AND vs.vendor_id = ANY(@vendor_ids)
|
||||||
|
ORDER BY vs.vendor_id, vs.name ASC`,
|
||||||
|
pgx.NamedArgs{"snapshot_id": snapshotID, "vendor_ids": vendorIDs})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot load snapshot services: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
result := make(map[string][]docgen.VendorListService)
|
||||||
|
for rows.Next() {
|
||||||
|
var vendorID, name, desc string
|
||||||
|
if err := rows.Scan(&vendorID, &name, &desc); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot scan service: %w", err)
|
||||||
|
}
|
||||||
|
result[vendorID] = append(result[vendorID], docgen.VendorListService{Name: name, Description: desc})
|
||||||
|
}
|
||||||
|
return result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSnapshotContacts(ctx context.Context, tx pg.Tx, snapshotID string, vendorIDs []string) (map[string][]docgen.VendorListContact, error) {
|
||||||
|
rows, err := tx.Query(ctx,
|
||||||
|
`SELECT vc.vendor_id,
|
||||||
|
COALESCE(vc.full_name, 'Not specified'),
|
||||||
|
COALESCE(vc.email, 'Not specified'),
|
||||||
|
COALESCE(vc.phone, 'Not specified'),
|
||||||
|
COALESCE(vc.role, 'Not specified')
|
||||||
|
FROM vendor_contacts vc
|
||||||
|
WHERE vc.snapshot_id = @snapshot_id AND vc.vendor_id = ANY(@vendor_ids)
|
||||||
|
ORDER BY vc.vendor_id, vc.full_name ASC`,
|
||||||
|
pgx.NamedArgs{"snapshot_id": snapshotID, "vendor_ids": vendorIDs})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot load snapshot contacts: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
result := make(map[string][]docgen.VendorListContact)
|
||||||
|
for rows.Next() {
|
||||||
|
var vendorID, name, email, phone, role string
|
||||||
|
if err := rows.Scan(&vendorID, &name, &email, &phone, &role); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot scan contact: %w", err)
|
||||||
|
}
|
||||||
|
result[vendorID] = append(result[vendorID], docgen.VendorListContact{
|
||||||
|
FullName: name, Email: email, Phone: phone, Role: role,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSnapshotRiskAssessments(ctx context.Context, tx pg.Tx, snapshotID string, vendorIDs []string) (map[string][]docgen.VendorListRiskAssessment, error) {
|
||||||
|
rows, err := tx.Query(ctx,
|
||||||
|
`SELECT vra.vendor_id, vra.created_at, vra.expires_at, vra.data_sensitivity, vra.business_impact, COALESCE(vra.notes, 'Not specified')
|
||||||
|
FROM vendor_risk_assessments vra
|
||||||
|
WHERE vra.snapshot_id = @snapshot_id AND vra.vendor_id = ANY(@vendor_ids)
|
||||||
|
ORDER BY vra.vendor_id, vra.created_at DESC`,
|
||||||
|
pgx.NamedArgs{"snapshot_id": snapshotID, "vendor_ids": vendorIDs})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot load snapshot risk assessments: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
result := make(map[string][]docgen.VendorListRiskAssessment)
|
||||||
|
for rows.Next() {
|
||||||
|
var vendorID, sensitivity, impact, notes string
|
||||||
|
var assessedAt, expiresAt time.Time
|
||||||
|
if err := rows.Scan(&vendorID, &assessedAt, &expiresAt, &sensitivity, &impact, ¬es); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot scan risk assessment: %w", err)
|
||||||
|
}
|
||||||
|
result[vendorID] = append(result[vendorID], docgen.VendorListRiskAssessment{
|
||||||
|
AssessedAt: assessedAt.Format("2006-01-02"),
|
||||||
|
ExpiresAt: expiresAt.Format("2006-01-02"),
|
||||||
|
DataSensitivity: sensitivity,
|
||||||
|
BusinessImpact: impact,
|
||||||
|
Notes: notes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSnapshotComplianceReports(ctx context.Context, tx pg.Tx, snapshotID string, vendorIDs []string) (map[string][]docgen.VendorListComplianceReport, error) {
|
||||||
|
rows, err := tx.Query(ctx,
|
||||||
|
`SELECT vcr.vendor_id, vcr.report_name, vcr.report_date, vcr.valid_until
|
||||||
|
FROM vendor_compliance_reports vcr
|
||||||
|
WHERE vcr.snapshot_id = @snapshot_id AND vcr.vendor_id = ANY(@vendor_ids)
|
||||||
|
ORDER BY vcr.vendor_id, vcr.report_date DESC`,
|
||||||
|
pgx.NamedArgs{"snapshot_id": snapshotID, "vendor_ids": vendorIDs})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot load snapshot compliance reports: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
result := make(map[string][]docgen.VendorListComplianceReport)
|
||||||
|
for rows.Next() {
|
||||||
|
var vendorID, name string
|
||||||
|
var reportDate time.Time
|
||||||
|
var validUntil *time.Time
|
||||||
|
if err := rows.Scan(&vendorID, &name, &reportDate, &validUntil); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot scan compliance report: %w", err)
|
||||||
|
}
|
||||||
|
vu := "Not specified"
|
||||||
|
if validUntil != nil {
|
||||||
|
vu = validUntil.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
result[vendorID] = append(result[vendorID], docgen.VendorListComplianceReport{
|
||||||
|
ReportName: name, ReportDate: reportDate.Format("2006-01-02"), ValidUntil: vu,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSnapshotBAAs(ctx context.Context, tx pg.Tx, snapshotID string, vendorIDs []string) (map[string]*docgen.VendorListAgreement, error) {
|
||||||
|
rows, err := tx.Query(ctx,
|
||||||
|
`SELECT vbaa.vendor_id, vbaa.valid_from, vbaa.valid_until
|
||||||
|
FROM vendor_business_associate_agreements vbaa
|
||||||
|
WHERE vbaa.snapshot_id = @snapshot_id AND vbaa.vendor_id = ANY(@vendor_ids)`,
|
||||||
|
pgx.NamedArgs{"snapshot_id": snapshotID, "vendor_ids": vendorIDs})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot load snapshot BAAs: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
result := make(map[string]*docgen.VendorListAgreement)
|
||||||
|
for rows.Next() {
|
||||||
|
var vendorID string
|
||||||
|
var validFrom, validUntil *time.Time
|
||||||
|
if err := rows.Scan(&vendorID, &validFrom, &validUntil); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot scan BAA: %w", err)
|
||||||
|
}
|
||||||
|
result[vendorID] = &docgen.VendorListAgreement{
|
||||||
|
ValidFrom: fmtTime(validFrom), ValidUntil: fmtTime(validUntil),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSnapshotDPAs(ctx context.Context, tx pg.Tx, snapshotID string, vendorIDs []string) (map[string]*docgen.VendorListAgreement, error) {
|
||||||
|
rows, err := tx.Query(ctx,
|
||||||
|
`SELECT vdpa.vendor_id, vdpa.valid_from, vdpa.valid_until
|
||||||
|
FROM vendor_data_privacy_agreements vdpa
|
||||||
|
WHERE vdpa.snapshot_id = @snapshot_id AND vdpa.vendor_id = ANY(@vendor_ids)`,
|
||||||
|
pgx.NamedArgs{"snapshot_id": snapshotID, "vendor_ids": vendorIDs})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot load snapshot DPAs: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
result := make(map[string]*docgen.VendorListAgreement)
|
||||||
|
for rows.Next() {
|
||||||
|
var vendorID string
|
||||||
|
var validFrom, validUntil *time.Time
|
||||||
|
if err := rows.Scan(&vendorID, &validFrom, &validUntil); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot scan DPA: %w", err)
|
||||||
|
}
|
||||||
|
result[vendorID] = &docgen.VendorListAgreement{
|
||||||
|
ValidFrom: fmtTime(validFrom), ValidUntil: fmtTime(validUntil),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func deref(s *string) string {
|
||||||
|
if s == nil || *s == "" {
|
||||||
|
return "Not specified"
|
||||||
|
}
|
||||||
|
return *s
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinOrDefault(items []string) string {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return "Not specified"
|
||||||
|
}
|
||||||
|
return strings.Join(items, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func fmtTime(t *time.Time) string {
|
||||||
|
if t == nil {
|
||||||
|
return "Not specified"
|
||||||
|
}
|
||||||
|
return t.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatCategory(c string) string {
|
||||||
|
switch c {
|
||||||
|
case "ANALYTICS":
|
||||||
|
return "Analytics"
|
||||||
|
case "CLOUD_MONITORING":
|
||||||
|
return "Cloud Monitoring"
|
||||||
|
case "CLOUD_PROVIDER":
|
||||||
|
return "Cloud Provider"
|
||||||
|
case "COLLABORATION":
|
||||||
|
return "Collaboration"
|
||||||
|
case "CUSTOMER_SUPPORT":
|
||||||
|
return "Customer Support"
|
||||||
|
case "DATA_STORAGE_AND_PROCESSING":
|
||||||
|
return "Data Storage and Processing"
|
||||||
|
case "DOCUMENT_MANAGEMENT":
|
||||||
|
return "Document Management"
|
||||||
|
case "EMPLOYEE_MANAGEMENT":
|
||||||
|
return "Employee Management"
|
||||||
|
case "ENGINEERING":
|
||||||
|
return "Engineering"
|
||||||
|
case "FINANCE":
|
||||||
|
return "Finance"
|
||||||
|
case "IDENTITY_PROVIDER":
|
||||||
|
return "Identity Provider"
|
||||||
|
case "IT":
|
||||||
|
return "IT"
|
||||||
|
case "MARKETING":
|
||||||
|
return "Marketing"
|
||||||
|
case "OFFICE_OPERATIONS":
|
||||||
|
return "Office Operations"
|
||||||
|
case "OTHER":
|
||||||
|
return "Other"
|
||||||
|
case "PASSWORD_MANAGEMENT":
|
||||||
|
return "Password Management"
|
||||||
|
case "PRODUCT_AND_DESIGN":
|
||||||
|
return "Product and Design"
|
||||||
|
case "PROFESSIONAL_SERVICES":
|
||||||
|
return "Professional Services"
|
||||||
|
case "RECRUITING":
|
||||||
|
return "Recruiting"
|
||||||
|
case "SALES":
|
||||||
|
return "Sales"
|
||||||
|
case "SECURITY":
|
||||||
|
return "Security"
|
||||||
|
case "VERSION_CONTROL":
|
||||||
|
return "Version Control"
|
||||||
|
default:
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPgClientFromDSN(dsn string) (*pg.Client, error) {
|
||||||
|
u, err := url.Parse(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse DSN")
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts []pg.Option
|
||||||
|
|
||||||
|
if u.Host != "" {
|
||||||
|
opts = append(opts, pg.WithAddr(u.Host))
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.User != nil {
|
||||||
|
opts = append(opts, pg.WithUser(u.User.Username()))
|
||||||
|
if password, ok := u.User.Password(); ok {
|
||||||
|
opts = append(opts, pg.WithPassword(password))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(u.Path) > 1 {
|
||||||
|
opts = append(opts, pg.WithDatabase(u.Path[1:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return pg.NewClient(opts...)
|
||||||
|
}
|
||||||
@@ -104,7 +104,7 @@ func TestSnapshot_Delete(t *testing.T) {
|
|||||||
"input": map[string]any{
|
"input": map[string]any{
|
||||||
"organizationId": owner.GetOrganizationID().String(),
|
"organizationId": owner.GetOrganizationID().String(),
|
||||||
"name": fmt.Sprintf("Snapshot to Delete %d", time.Now().UnixNano()),
|
"name": fmt.Sprintf("Snapshot to Delete %d", time.Now().UnixNano()),
|
||||||
"type": "VENDORS",
|
"type": "RISKS",
|
||||||
},
|
},
|
||||||
}, &createResult)
|
}, &createResult)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -140,7 +140,7 @@ func TestSnapshot_List(t *testing.T) {
|
|||||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
|
||||||
// Create multiple snapshots
|
// Create multiple snapshots
|
||||||
snapshotTypes := []string{"RISKS", "VENDORS"}
|
snapshotTypes := []string{"RISKS"}
|
||||||
for i, snapshotType := range snapshotTypes {
|
for i, snapshotType := range snapshotTypes {
|
||||||
query := `
|
query := `
|
||||||
mutation CreateSnapshot($input: CreateSnapshotInput!) {
|
mutation CreateSnapshot($input: CreateSnapshotInput!) {
|
||||||
@@ -219,7 +219,7 @@ func TestSnapshot_Types(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
|
||||||
snapshotTypes := []string{"RISKS", "VENDORS"}
|
snapshotTypes := []string{"RISKS"}
|
||||||
|
|
||||||
for _, snapshotType := range snapshotTypes {
|
for _, snapshotType := range snapshotTypes {
|
||||||
t.Run(snapshotType, func(t *testing.T) {
|
t.Run(snapshotType, func(t *testing.T) {
|
||||||
|
|||||||
336
e2e/console/vendor_publish_test.go
Normal file
336
e2e/console/vendor_publish_test.go
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package console_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.probo.inc/probo/e2e/internal/factory"
|
||||||
|
"go.probo.inc/probo/e2e/internal/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVendor_PublishVendorList(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"publish without approvers publishes immediately",
|
||||||
|
func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
factory.CreateVendor(owner, factory.Attrs{"name": "Test Vendor"})
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
mutation($input: PublishVendorListInput!) {
|
||||||
|
publishVendorList(input: $input) {
|
||||||
|
documentEdge {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
writeMode
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
documentVersionEdge {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
documentType
|
||||||
|
status
|
||||||
|
major
|
||||||
|
minor
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
PublishVendorList struct {
|
||||||
|
DocumentEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
WriteMode string `json:"writeMode"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentEdge"`
|
||||||
|
DocumentVersionEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
DocumentType string `json:"documentType"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Major int `json:"major"`
|
||||||
|
Minor int `json:"minor"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentVersionEdge"`
|
||||||
|
} `json:"publishVendorList"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := owner.Execute(
|
||||||
|
query,
|
||||||
|
map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": owner.GetOrganizationID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&result,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
doc := result.PublishVendorList.DocumentEdge.Node
|
||||||
|
assert.NotEmpty(t, doc.ID)
|
||||||
|
assert.Equal(t, "GENERATED", doc.WriteMode)
|
||||||
|
assert.Equal(t, "ACTIVE", doc.Status)
|
||||||
|
|
||||||
|
ver := result.PublishVendorList.DocumentVersionEdge.Node
|
||||||
|
assert.NotEmpty(t, ver.ID)
|
||||||
|
assert.Equal(t, "REGISTER", ver.DocumentType)
|
||||||
|
assert.Equal(t, "PUBLISHED", ver.Status)
|
||||||
|
assert.Equal(t, 1, ver.Major)
|
||||||
|
assert.Equal(t, 0, ver.Minor)
|
||||||
|
assert.Contains(t, ver.Content, "Purpose")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"publish with approvers creates draft pending approval",
|
||||||
|
func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
mutation($input: PublishVendorListInput!) {
|
||||||
|
publishVendorList(input: $input) {
|
||||||
|
documentEdge {
|
||||||
|
node { id writeMode }
|
||||||
|
}
|
||||||
|
documentVersionEdge {
|
||||||
|
node { id status major }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
PublishVendorList struct {
|
||||||
|
DocumentEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
WriteMode string `json:"writeMode"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentEdge"`
|
||||||
|
DocumentVersionEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Major int `json:"major"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentVersionEdge"`
|
||||||
|
} `json:"publishVendorList"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := owner.Execute(
|
||||||
|
query,
|
||||||
|
map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": owner.GetOrganizationID(),
|
||||||
|
"approverIds": []string{owner.GetProfileID().String()},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&result,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
doc := result.PublishVendorList.DocumentEdge.Node
|
||||||
|
assert.NotEmpty(t, doc.ID)
|
||||||
|
assert.Equal(t, "GENERATED", doc.WriteMode)
|
||||||
|
|
||||||
|
ver := result.PublishVendorList.DocumentVersionEdge.Node
|
||||||
|
assert.NotEmpty(t, ver.ID)
|
||||||
|
assert.Equal(t, "PENDING_APPROVAL", ver.Status)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"second publish reuses document and bumps major version",
|
||||||
|
func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
factory.CreateVendor(owner, factory.Attrs{"name": "Reuse Vendor"})
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
mutation($input: PublishVendorListInput!) {
|
||||||
|
publishVendorList(input: $input) {
|
||||||
|
documentEdge { node { id } }
|
||||||
|
documentVersionEdge { node { id major } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var r1, r2 struct {
|
||||||
|
PublishVendorList struct {
|
||||||
|
DocumentEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentEdge"`
|
||||||
|
DocumentVersionEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Major int `json:"major"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentVersionEdge"`
|
||||||
|
} `json:"publishVendorList"`
|
||||||
|
}
|
||||||
|
|
||||||
|
input := map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": owner.GetOrganizationID(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := owner.Execute(query, input, &r1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = owner.Execute(query, input, &r2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
r1.PublishVendorList.DocumentEdge.Node.ID,
|
||||||
|
r2.PublishVendorList.DocumentEdge.Node.ID,
|
||||||
|
"should reuse same document",
|
||||||
|
)
|
||||||
|
assert.Equal(t, 1, r1.PublishVendorList.DocumentVersionEdge.Node.Major)
|
||||||
|
assert.Equal(t, 2, r2.PublishVendorList.DocumentVersionEdge.Node.Major)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"organization vendorsDocument links to published document",
|
||||||
|
func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
factory.CreateVendor(owner, factory.Attrs{"name": "Linked Vendor"})
|
||||||
|
|
||||||
|
const publishQuery = `
|
||||||
|
mutation($input: PublishVendorListInput!) {
|
||||||
|
publishVendorList(input: $input) {
|
||||||
|
documentEdge { node { id } }
|
||||||
|
documentVersionEdge { node { id } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var publishResult struct {
|
||||||
|
PublishVendorList struct {
|
||||||
|
DocumentEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentEdge"`
|
||||||
|
DocumentVersionEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentVersionEdge"`
|
||||||
|
} `json:"publishVendorList"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := owner.Execute(
|
||||||
|
publishQuery,
|
||||||
|
map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": owner.GetOrganizationID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&publishResult,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
docID := publishResult.PublishVendorList.DocumentEdge.Node.ID
|
||||||
|
|
||||||
|
const orgQuery = `
|
||||||
|
query($id: ID!) {
|
||||||
|
node(id: $id) {
|
||||||
|
... on Organization {
|
||||||
|
id
|
||||||
|
vendorsDocument { id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var orgResult struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
VendorsDocument *struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"vendorsDocument"`
|
||||||
|
} `json:"node"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = owner.Execute(
|
||||||
|
orgQuery,
|
||||||
|
map[string]any{"id": owner.GetOrganizationID()},
|
||||||
|
&orgResult,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, orgResult.Node.VendorsDocument)
|
||||||
|
assert.Equal(t, docID, orgResult.Node.VendorsDocument.ID)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVendor_PublishVendorList_RBAC(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner)
|
||||||
|
|
||||||
|
factory.CreateVendor(owner, factory.Attrs{"name": "RBAC Vendor"})
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
mutation($input: PublishVendorListInput!) {
|
||||||
|
publishVendorList(input: $input) {
|
||||||
|
documentEdge { node { id } }
|
||||||
|
documentVersionEdge { node { id } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"viewer cannot publish vendor list",
|
||||||
|
func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
err := viewer.ExecuteShouldFail(
|
||||||
|
query,
|
||||||
|
map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": owner.GetOrganizationID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
testutil.RequireForbiddenError(t, err)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -29,8 +29,8 @@ func TestMCP_Snapshot(t *testing.T) {
|
|||||||
mc := testutil.NewMCPClient(t, owner)
|
mc := testutil.NewMCPClient(t, owner)
|
||||||
orgID := owner.GetOrganizationID().String()
|
orgID := owner.GetOrganizationID().String()
|
||||||
|
|
||||||
// Create a vendor so the snapshot has data
|
// Create a risk so the snapshot has data
|
||||||
factory.CreateVendor(owner)
|
factory.CreateRisk(owner)
|
||||||
|
|
||||||
// Take snapshot
|
// Take snapshot
|
||||||
var takeResult struct {
|
var takeResult struct {
|
||||||
@@ -41,7 +41,7 @@ func TestMCP_Snapshot(t *testing.T) {
|
|||||||
mc.CallToolInto("takeSnapshot", map[string]any{
|
mc.CallToolInto("takeSnapshot", map[string]any{
|
||||||
"organizationId": orgID,
|
"organizationId": orgID,
|
||||||
"name": factory.SafeName("Snapshot"),
|
"name": factory.SafeName("Snapshot"),
|
||||||
"snapshotsType": "VENDORS",
|
"snapshotsType": "RISKS",
|
||||||
}, &takeResult)
|
}, &takeResult)
|
||||||
require.NotEmpty(t, takeResult.Snapshot.ID)
|
require.NotEmpty(t, takeResult.Snapshot.ID)
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ type Translator = (s: string) => string;
|
|||||||
|
|
||||||
export const snapshotTypes = [
|
export const snapshotTypes = [
|
||||||
"RISKS",
|
"RISKS",
|
||||||
"VENDORS",
|
|
||||||
"PROCESSING_ACTIVITIES",
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export function getSnapshotTypeLabel(__: Translator, type: string | null | undefined) {
|
export function getSnapshotTypeLabel(__: Translator, type: string | null | undefined) {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import * as updateBusinessAssociateAgreementOp from './updateBusinessAssociateAg
|
|||||||
import * as getDataPrivacyAgreementOp from './getDataPrivacyAgreement.operation';
|
import * as getDataPrivacyAgreementOp from './getDataPrivacyAgreement.operation';
|
||||||
import * as deleteDataPrivacyAgreementOp from './deleteDataPrivacyAgreement.operation';
|
import * as deleteDataPrivacyAgreementOp from './deleteDataPrivacyAgreement.operation';
|
||||||
import * as updateDataPrivacyAgreementOp from './updateDataPrivacyAgreement.operation';
|
import * as updateDataPrivacyAgreementOp from './updateDataPrivacyAgreement.operation';
|
||||||
|
import * as publishOp from './publish.operation';
|
||||||
|
|
||||||
export const description: INodeProperties[] = [
|
export const description: INodeProperties[] = [
|
||||||
{
|
{
|
||||||
@@ -178,6 +179,12 @@ export const description: INodeProperties[] = [
|
|||||||
description: 'Get a vendor service',
|
description: 'Get a vendor service',
|
||||||
action: 'Get a vendor service',
|
action: 'Get a vendor service',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Publish List',
|
||||||
|
value: 'publish',
|
||||||
|
description: 'Publish the vendor register as a document version',
|
||||||
|
action: 'Publish the vendor register',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Update',
|
name: 'Update',
|
||||||
value: 'update',
|
value: 'update',
|
||||||
@@ -237,6 +244,7 @@ export const description: INodeProperties[] = [
|
|||||||
...getDataPrivacyAgreementOp.description,
|
...getDataPrivacyAgreementOp.description,
|
||||||
...deleteDataPrivacyAgreementOp.description,
|
...deleteDataPrivacyAgreementOp.description,
|
||||||
...updateDataPrivacyAgreementOp.description,
|
...updateDataPrivacyAgreementOp.description,
|
||||||
|
...publishOp.description,
|
||||||
];
|
];
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -266,4 +274,5 @@ export {
|
|||||||
getDataPrivacyAgreementOp as getDataPrivacyAgreement,
|
getDataPrivacyAgreementOp as getDataPrivacyAgreement,
|
||||||
deleteDataPrivacyAgreementOp as deleteDataPrivacyAgreement,
|
deleteDataPrivacyAgreementOp as deleteDataPrivacyAgreement,
|
||||||
updateDataPrivacyAgreementOp as updateDataPrivacyAgreement,
|
updateDataPrivacyAgreementOp as updateDataPrivacyAgreement,
|
||||||
|
publishOp as publish,
|
||||||
};
|
};
|
||||||
|
|||||||
101
packages/n8n-node/nodes/Probo/actions/vendor/publish.operation.ts
vendored
Normal file
101
packages/n8n-node/nodes/Probo/actions/vendor/publish.operation.ts
vendored
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||||
|
import { proboApiRequest } from '../../GenericFunctions';
|
||||||
|
|
||||||
|
export const description: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Organization ID',
|
||||||
|
name: 'organizationId',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['vendor'],
|
||||||
|
operation: ['publish'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The ID of the organization whose vendor list to publish',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Approver IDs',
|
||||||
|
name: 'approverIds',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['vendor'],
|
||||||
|
operation: ['publish'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'Comma-separated list of approver profile IDs',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function execute(
|
||||||
|
this: IExecuteFunctions,
|
||||||
|
itemIndex: number,
|
||||||
|
): Promise<INodeExecutionData> {
|
||||||
|
const organizationId = this.getNodeParameter('organizationId', itemIndex) as string;
|
||||||
|
const approverIds = this.getNodeParameter('approverIds', itemIndex, '') as string;
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
mutation PublishVendorList($input: PublishVendorListInput!) {
|
||||||
|
publishVendorList(input: $input) {
|
||||||
|
documentEdge {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
currentPublishedMajor
|
||||||
|
currentPublishedMinor
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
documentVersionEdge {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
major
|
||||||
|
minor
|
||||||
|
status
|
||||||
|
classification
|
||||||
|
documentType
|
||||||
|
publishedAt
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const input: Record<string, unknown> = { organizationId };
|
||||||
|
|
||||||
|
if (approverIds) {
|
||||||
|
input.approverIds = approverIds
|
||||||
|
.split(',')
|
||||||
|
.map(id => id.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseData = await proboApiRequest.call(this, query, { input });
|
||||||
|
|
||||||
|
return {
|
||||||
|
json: responseData,
|
||||||
|
pairedItem: { item: itemIndex },
|
||||||
|
};
|
||||||
|
}
|
||||||
148
pkg/cmd/vendormgmt/publish/publish.go
Normal file
148
pkg/cmd/vendormgmt/publish/publish.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package publish
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.probo.inc/probo/pkg/cli/api"
|
||||||
|
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const publishMutation = `
|
||||||
|
mutation($input: PublishVendorListInput!) {
|
||||||
|
publishVendorList(input: $input) {
|
||||||
|
documentEdge {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
documentVersionEdge {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
major
|
||||||
|
minor
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
type publishResponse struct {
|
||||||
|
PublishVendorList struct {
|
||||||
|
DocumentEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentEdge"`
|
||||||
|
DocumentVersionEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Major int `json:"major"`
|
||||||
|
Minor int `json:"minor"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentVersionEdge"`
|
||||||
|
} `json:"publishVendorList"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCmdPublish(f *cmdutil.Factory) *cobra.Command {
|
||||||
|
var (
|
||||||
|
flagOrg string
|
||||||
|
flagApprover []string
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "publish",
|
||||||
|
Short: "Publish the vendor register as a document version",
|
||||||
|
Example: ` # Publish the vendor register
|
||||||
|
prb vendor publish --org ORG_ID
|
||||||
|
|
||||||
|
# Publish with approvers
|
||||||
|
prb vendor publish --org ORG_ID --approver PROFILE_ID1 --approver PROFILE_ID2`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cfg, err := f.Config()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
host, hc, err := cfg.DefaultHost()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if flagOrg == "" {
|
||||||
|
flagOrg = hc.Organization
|
||||||
|
}
|
||||||
|
if flagOrg == "" {
|
||||||
|
return fmt.Errorf("organization is required: pass --org or run `prb auth login`")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := api.NewClient(
|
||||||
|
host,
|
||||||
|
hc.Token,
|
||||||
|
"/api/console/v1/graphql",
|
||||||
|
cfg.HTTPTimeoutDuration(),
|
||||||
|
cmdutil.TokenRefreshOption(cfg, host, hc),
|
||||||
|
)
|
||||||
|
|
||||||
|
input := map[string]any{
|
||||||
|
"organizationId": flagOrg,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(flagApprover) > 0 {
|
||||||
|
input["approverIds"] = flagApprover
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := client.Do(
|
||||||
|
publishMutation,
|
||||||
|
map[string]any{"input": input},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp publishResponse
|
||||||
|
if err := json.Unmarshal(data, &resp); err != nil {
|
||||||
|
return fmt.Errorf("cannot parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
v := resp.PublishVendorList.DocumentVersionEdge.Node
|
||||||
|
_, _ = fmt.Fprintf(
|
||||||
|
f.IOStreams.Out,
|
||||||
|
"Published vendor register %s (v%d.%d)\n",
|
||||||
|
v.Title,
|
||||||
|
v.Major,
|
||||||
|
v.Minor,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
|
||||||
|
cmd.Flags().StringArrayVar(&flagApprover, "approver", nil, "Approver profile ID (can be repeated)")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/cmd/vendormgmt/create"
|
"go.probo.inc/probo/pkg/cmd/vendormgmt/create"
|
||||||
"go.probo.inc/probo/pkg/cmd/vendormgmt/delete"
|
"go.probo.inc/probo/pkg/cmd/vendormgmt/delete"
|
||||||
"go.probo.inc/probo/pkg/cmd/vendormgmt/list"
|
"go.probo.inc/probo/pkg/cmd/vendormgmt/list"
|
||||||
|
"go.probo.inc/probo/pkg/cmd/vendormgmt/publish"
|
||||||
"go.probo.inc/probo/pkg/cmd/vendormgmt/update"
|
"go.probo.inc/probo/pkg/cmd/vendormgmt/update"
|
||||||
"go.probo.inc/probo/pkg/cmd/vendormgmt/view"
|
"go.probo.inc/probo/pkg/cmd/vendormgmt/view"
|
||||||
)
|
)
|
||||||
@@ -37,6 +38,7 @@ func NewCmdVendor(f *cmdutil.Factory) *cobra.Command {
|
|||||||
cmd.AddCommand(update.NewCmdUpdate(f))
|
cmd.AddCommand(update.NewCmdUpdate(f))
|
||||||
cmd.AddCommand(delete.NewCmdDelete(f))
|
cmd.AddCommand(delete.NewCmdDelete(f))
|
||||||
cmd.AddCommand(assess.NewCmdAssess(f))
|
cmd.AddCommand(assess.NewCmdAssess(f))
|
||||||
|
cmd.AddCommand(publish.NewCmdPublish(f))
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
16
pkg/coredata/migrations/20260428T152537Z.sql
Normal file
16
pkg/coredata/migrations/20260428T152537Z.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
-- Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
--
|
||||||
|
-- Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
-- purpose with or without fee is hereby granted, provided that the above
|
||||||
|
-- copyright notice and this permission notice appear in all copies.
|
||||||
|
--
|
||||||
|
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
-- PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
ALTER TABLE generated_documents
|
||||||
|
ADD COLUMN vendors_document_id TEXT REFERENCES documents(id) ON DELETE SET NULL;
|
||||||
@@ -124,7 +124,7 @@ FROM
|
|||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND organization_id = @organization_id
|
AND organization_id = @organization_id
|
||||||
AND type NOT IN ('STATEMENTS_OF_APPLICABILITY', 'DATA', 'ASSETS')
|
AND type = 'RISKS'
|
||||||
AND %s
|
AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ FROM
|
|||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND organization_id = @organization_id
|
AND organization_id = @organization_id
|
||||||
AND type NOT IN ('STATEMENTS_OF_APPLICABILITY', 'DATA', 'ASSETS')
|
AND type = 'RISKS'
|
||||||
AND %s
|
AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ type (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
SnapshotsTypeRisks SnapshotsType = "RISKS"
|
SnapshotsTypeRisks SnapshotsType = "RISKS"
|
||||||
SnapshotsTypeVendors SnapshotsType = "VENDORS"
|
|
||||||
SnapshotsTypeAssets SnapshotsType = "ASSETS"
|
SnapshotsTypeAssets SnapshotsType = "ASSETS"
|
||||||
SnapshotsTypeData SnapshotsType = "DATA"
|
SnapshotsTypeData SnapshotsType = "DATA"
|
||||||
SnapshotsTypeFindings SnapshotsType = "FINDINGS"
|
SnapshotsTypeFindings SnapshotsType = "FINDINGS"
|
||||||
@@ -37,7 +36,6 @@ const (
|
|||||||
func SnapshotsTypes() []SnapshotsType {
|
func SnapshotsTypes() []SnapshotsType {
|
||||||
return []SnapshotsType{
|
return []SnapshotsType{
|
||||||
SnapshotsTypeRisks,
|
SnapshotsTypeRisks,
|
||||||
SnapshotsTypeVendors,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,8 +57,6 @@ func (st *SnapshotsType) Scan(value any) error {
|
|||||||
switch s {
|
switch s {
|
||||||
case SnapshotsTypeRisks.String():
|
case SnapshotsTypeRisks.String():
|
||||||
*st = SnapshotsTypeRisks
|
*st = SnapshotsTypeRisks
|
||||||
case SnapshotsTypeVendors.String():
|
|
||||||
*st = SnapshotsTypeVendors
|
|
||||||
case SnapshotsTypeAssets.String():
|
case SnapshotsTypeAssets.String():
|
||||||
*st = SnapshotsTypeAssets
|
*st = SnapshotsTypeAssets
|
||||||
case SnapshotsTypeData.String():
|
case SnapshotsTypeData.String():
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ func GetSnapshottable(snapshotType SnapshotsType) (Snapshottable, error) {
|
|||||||
switch snapshotType {
|
switch snapshotType {
|
||||||
case SnapshotsTypeRisks:
|
case SnapshotsTypeRisks:
|
||||||
return Risks{}, nil
|
return Risks{}, nil
|
||||||
case SnapshotsTypeVendors:
|
|
||||||
return Vendors{}, nil
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported snapshot type: %s", snapshotType)
|
return nil, fmt.Errorf("unsupported snapshot type: %s", snapshotType)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,113 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/page"
|
"go.probo.inc/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (v Vendor) GetGeneratedDocumentID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
organizationID gid.GID,
|
||||||
|
) (*gid.GID, error) {
|
||||||
|
var documentID *gid.GID
|
||||||
|
|
||||||
|
err := conn.QueryRow(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
vendors_document_id
|
||||||
|
FROM
|
||||||
|
generated_documents
|
||||||
|
WHERE
|
||||||
|
organization_id = @organization_id
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{"organization_id": organizationID},
|
||||||
|
).Scan(&documentID)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot get vendor list document ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return documentID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vendor) UpsertGeneratedDocumentID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Tx,
|
||||||
|
organizationID gid.GID,
|
||||||
|
tenantID gid.TenantID,
|
||||||
|
documentID gid.GID,
|
||||||
|
) error {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
_, err := conn.Exec(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
INSERT INTO generated_documents (
|
||||||
|
organization_id,
|
||||||
|
tenant_id,
|
||||||
|
vendors_document_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES (
|
||||||
|
@organization_id,
|
||||||
|
@tenant_id,
|
||||||
|
@vendors_document_id,
|
||||||
|
@created_at,
|
||||||
|
@updated_at
|
||||||
|
)
|
||||||
|
ON CONFLICT (organization_id) DO UPDATE
|
||||||
|
SET
|
||||||
|
vendors_document_id = @vendors_document_id,
|
||||||
|
updated_at = @updated_at
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"organization_id": organizationID,
|
||||||
|
"tenant_id": tenantID,
|
||||||
|
"vendors_document_id": documentID,
|
||||||
|
"created_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot upsert vendor list document ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vendor) ClearGeneratedDocumentID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Tx,
|
||||||
|
documentIDs []gid.GID,
|
||||||
|
) error {
|
||||||
|
ids := make([]string, len(documentIDs))
|
||||||
|
for i, id := range documentIDs {
|
||||||
|
ids[i] = id.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := conn.Exec(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
UPDATE
|
||||||
|
generated_documents
|
||||||
|
SET
|
||||||
|
vendors_document_id = NULL,
|
||||||
|
updated_at = @now
|
||||||
|
WHERE
|
||||||
|
vendors_document_id = ANY(@ids)
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"ids": ids,
|
||||||
|
"now": time.Now(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot clear vendor list document references: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Vendor struct {
|
Vendor struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
@@ -52,17 +159,11 @@ type (
|
|||||||
SecurityPageURL *string `db:"security_page_url"`
|
SecurityPageURL *string `db:"security_page_url"`
|
||||||
TrustPageURL *string `db:"trust_page_url"`
|
TrustPageURL *string `db:"trust_page_url"`
|
||||||
ShowOnTrustCenter bool `db:"show_on_trust_center"`
|
ShowOnTrustCenter bool `db:"show_on_trust_center"`
|
||||||
SnapshotID *gid.GID `db:"snapshot_id"`
|
|
||||||
SourceID *gid.GID `db:"source_id"`
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
Vendors []*Vendor
|
Vendors []*Vendor
|
||||||
|
|
||||||
VendorSnapshotter interface {
|
|
||||||
InsertVendorSnapshots(ctx context.Context, conn pg.Tx, scope Scoper, organizationID, snapshotID gid.GID) error
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (v Vendor) CursorKey(orderBy VendorOrderField) page.CursorKey {
|
func (v Vendor) CursorKey(orderBy VendorOrderField) page.CursorKey {
|
||||||
@@ -123,8 +224,6 @@ SELECT
|
|||||||
security_page_url,
|
security_page_url,
|
||||||
trust_page_url,
|
trust_page_url,
|
||||||
show_on_trust_center,
|
show_on_trust_center,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -191,8 +290,6 @@ SELECT
|
|||||||
security_page_url,
|
security_page_url,
|
||||||
trust_page_url,
|
trust_page_url,
|
||||||
show_on_trust_center,
|
show_on_trust_center,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -253,8 +350,6 @@ INSERT INTO
|
|||||||
security_page_url,
|
security_page_url,
|
||||||
trust_page_url,
|
trust_page_url,
|
||||||
show_on_trust_center,
|
show_on_trust_center,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
)
|
)
|
||||||
@@ -282,8 +377,6 @@ VALUES (
|
|||||||
@security_page_url,
|
@security_page_url,
|
||||||
@trust_page_url,
|
@trust_page_url,
|
||||||
@show_on_trust_center,
|
@show_on_trust_center,
|
||||||
@snapshot_id,
|
|
||||||
@source_id,
|
|
||||||
@created_at,
|
@created_at,
|
||||||
@updated_at
|
@updated_at
|
||||||
)
|
)
|
||||||
@@ -313,8 +406,6 @@ VALUES (
|
|||||||
"security_page_url": v.SecurityPageURL,
|
"security_page_url": v.SecurityPageURL,
|
||||||
"trust_page_url": v.TrustPageURL,
|
"trust_page_url": v.TrustPageURL,
|
||||||
"show_on_trust_center": v.ShowOnTrustCenter,
|
"show_on_trust_center": v.ShowOnTrustCenter,
|
||||||
"snapshot_id": v.SnapshotID,
|
|
||||||
"source_id": v.SourceID,
|
|
||||||
"created_at": v.CreatedAt,
|
"created_at": v.CreatedAt,
|
||||||
"updated_at": v.UpdatedAt,
|
"updated_at": v.UpdatedAt,
|
||||||
}
|
}
|
||||||
@@ -355,6 +446,7 @@ FROM
|
|||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND organization_id = @organization_id
|
AND organization_id = @organization_id
|
||||||
|
AND snapshot_id IS NULL
|
||||||
AND %s
|
AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -375,6 +467,67 @@ WHERE
|
|||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *Vendors) LoadAllByOrganizationID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
scope Scoper,
|
||||||
|
organizationID gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
category,
|
||||||
|
headquarter_address,
|
||||||
|
legal_name,
|
||||||
|
website_url,
|
||||||
|
privacy_policy_url,
|
||||||
|
service_level_agreement_url,
|
||||||
|
data_processing_agreement_url,
|
||||||
|
business_associate_agreement_url,
|
||||||
|
subprocessors_list_url,
|
||||||
|
certifications,
|
||||||
|
countries,
|
||||||
|
business_owner_profile_id,
|
||||||
|
security_owner_profile_id,
|
||||||
|
status_page_url,
|
||||||
|
terms_of_service_url,
|
||||||
|
security_page_url,
|
||||||
|
trust_page_url,
|
||||||
|
show_on_trust_center,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
vendors
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND organization_id = @organization_id
|
||||||
|
AND snapshot_id IS NULL
|
||||||
|
ORDER BY name ASC
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query vendors: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vendors, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Vendor])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect vendors: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*v = vendors
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (v *Vendors) LoadByOrganizationID(
|
func (v *Vendors) LoadByOrganizationID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Querier,
|
conn pg.Querier,
|
||||||
@@ -408,8 +561,6 @@ SELECT
|
|||||||
security_page_url,
|
security_page_url,
|
||||||
trust_page_url,
|
trust_page_url,
|
||||||
show_on_trust_center,
|
show_on_trust_center,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -417,6 +568,7 @@ FROM
|
|||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND organization_id = @organization_id
|
AND organization_id = @organization_id
|
||||||
|
AND snapshot_id IS NULL
|
||||||
AND %s
|
AND %s
|
||||||
AND %s
|
AND %s
|
||||||
`
|
`
|
||||||
@@ -613,8 +765,6 @@ WITH vend AS (
|
|||||||
v.security_page_url,
|
v.security_page_url,
|
||||||
v.trust_page_url,
|
v.trust_page_url,
|
||||||
v.show_on_trust_center,
|
v.show_on_trust_center,
|
||||||
v.snapshot_id,
|
|
||||||
v.source_id,
|
|
||||||
v.created_at,
|
v.created_at,
|
||||||
v.updated_at
|
v.updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -648,8 +798,6 @@ SELECT
|
|||||||
security_page_url,
|
security_page_url,
|
||||||
trust_page_url,
|
trust_page_url,
|
||||||
show_on_trust_center,
|
show_on_trust_center,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -749,8 +897,6 @@ WITH vend AS (
|
|||||||
v.security_page_url,
|
v.security_page_url,
|
||||||
v.trust_page_url,
|
v.trust_page_url,
|
||||||
v.show_on_trust_center,
|
v.show_on_trust_center,
|
||||||
v.snapshot_id,
|
|
||||||
v.source_id,
|
|
||||||
v.created_at,
|
v.created_at,
|
||||||
v.updated_at
|
v.updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -784,8 +930,6 @@ SELECT
|
|||||||
security_page_url,
|
security_page_url,
|
||||||
trust_page_url,
|
trust_page_url,
|
||||||
show_on_trust_center,
|
show_on_trust_center,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -846,8 +990,6 @@ WITH vend AS (
|
|||||||
v.security_page_url,
|
v.security_page_url,
|
||||||
v.trust_page_url,
|
v.trust_page_url,
|
||||||
v.show_on_trust_center,
|
v.show_on_trust_center,
|
||||||
v.snapshot_id,
|
|
||||||
v.source_id,
|
|
||||||
v.created_at,
|
v.created_at,
|
||||||
v.updated_at
|
v.updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -881,8 +1023,6 @@ SELECT
|
|||||||
security_page_url,
|
security_page_url,
|
||||||
trust_page_url,
|
trust_page_url,
|
||||||
show_on_trust_center,
|
show_on_trust_center,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -944,8 +1084,6 @@ WITH vend AS (
|
|||||||
v.security_page_url,
|
v.security_page_url,
|
||||||
v.trust_page_url,
|
v.trust_page_url,
|
||||||
v.show_on_trust_center,
|
v.show_on_trust_center,
|
||||||
v.snapshot_id,
|
|
||||||
v.source_id,
|
|
||||||
v.created_at,
|
v.created_at,
|
||||||
v.updated_at
|
v.updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -979,8 +1117,6 @@ SELECT
|
|||||||
security_page_url,
|
security_page_url,
|
||||||
trust_page_url,
|
trust_page_url,
|
||||||
show_on_trust_center,
|
show_on_trust_center,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -1034,6 +1170,7 @@ filtered_vendors AS (
|
|||||||
vendors v
|
vendors v
|
||||||
WHERE
|
WHERE
|
||||||
v.tenant_id = @tenant_id
|
v.tenant_id = @tenant_id
|
||||||
|
AND v.snapshot_id IS NULL
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
pav.processing_activity_id,
|
pav.processing_activity_id,
|
||||||
@@ -1106,8 +1243,6 @@ WITH vend AS (
|
|||||||
v.security_page_url,
|
v.security_page_url,
|
||||||
v.trust_page_url,
|
v.trust_page_url,
|
||||||
v.show_on_trust_center,
|
v.show_on_trust_center,
|
||||||
v.snapshot_id,
|
|
||||||
v.source_id,
|
|
||||||
v.created_at,
|
v.created_at,
|
||||||
v.updated_at
|
v.updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -1141,8 +1276,6 @@ SELECT
|
|||||||
security_page_url,
|
security_page_url,
|
||||||
trust_page_url,
|
trust_page_url,
|
||||||
show_on_trust_center,
|
show_on_trust_center,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -1169,108 +1302,3 @@ ORDER BY name ASC
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v Vendors) Snapshot(ctx context.Context, conn pg.Tx, scope Scoper, organizationID, snapshotID gid.GID) error {
|
|
||||||
for _, snapshotter := range []VendorSnapshotter{
|
|
||||||
Vendors{},
|
|
||||||
VendorServices{},
|
|
||||||
VendorContacts{},
|
|
||||||
VendorRiskAssessments{},
|
|
||||||
VendorComplianceReports{},
|
|
||||||
VendorBusinessAssociateAgreements{},
|
|
||||||
VendorDataPrivacyAgreements{},
|
|
||||||
} {
|
|
||||||
if err := snapshotter.InsertVendorSnapshots(ctx, conn, scope, organizationID, snapshotID); err != nil {
|
|
||||||
return fmt.Errorf("cannot create vendor snapshots: (%T) %w", snapshotter, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Vendors) InsertVendorSnapshots(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Tx,
|
|
||||||
scope Scoper,
|
|
||||||
organizationID gid.GID,
|
|
||||||
snapshotID gid.GID,
|
|
||||||
) error {
|
|
||||||
query := `
|
|
||||||
INSERT INTO vendors (
|
|
||||||
tenant_id,
|
|
||||||
id,
|
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
organization_id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
category,
|
|
||||||
headquarter_address,
|
|
||||||
legal_name,
|
|
||||||
website_url,
|
|
||||||
privacy_policy_url,
|
|
||||||
service_level_agreement_url,
|
|
||||||
data_processing_agreement_url,
|
|
||||||
business_associate_agreement_url,
|
|
||||||
subprocessors_list_url,
|
|
||||||
certifications,
|
|
||||||
countries,
|
|
||||||
business_owner_profile_id,
|
|
||||||
security_owner_profile_id,
|
|
||||||
status_page_url,
|
|
||||||
terms_of_service_url,
|
|
||||||
security_page_url,
|
|
||||||
trust_page_url,
|
|
||||||
show_on_trust_center,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
@tenant_id,
|
|
||||||
generate_gid(decode_base64_unpadded(@tenant_id), @vendor_entity_type),
|
|
||||||
@snapshot_id,
|
|
||||||
v.id,
|
|
||||||
v.organization_id,
|
|
||||||
v.name,
|
|
||||||
v.description,
|
|
||||||
v.category,
|
|
||||||
v.headquarter_address,
|
|
||||||
v.legal_name,
|
|
||||||
v.website_url,
|
|
||||||
v.privacy_policy_url,
|
|
||||||
v.service_level_agreement_url,
|
|
||||||
v.data_processing_agreement_url,
|
|
||||||
v.business_associate_agreement_url,
|
|
||||||
v.subprocessors_list_url,
|
|
||||||
v.certifications,
|
|
||||||
v.countries,
|
|
||||||
v.business_owner_profile_id,
|
|
||||||
v.security_owner_profile_id,
|
|
||||||
v.status_page_url,
|
|
||||||
v.terms_of_service_url,
|
|
||||||
v.security_page_url,
|
|
||||||
v.trust_page_url,
|
|
||||||
v.show_on_trust_center,
|
|
||||||
v.created_at,
|
|
||||||
v.updated_at
|
|
||||||
FROM vendors v
|
|
||||||
WHERE %s AND organization_id = @organization_id AND snapshot_id IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"tenant_id": scope.GetTenantID(),
|
|
||||||
"snapshot_id": snapshotID,
|
|
||||||
"organization_id": organizationID,
|
|
||||||
"vendor_entity_type": VendorEntityType,
|
|
||||||
}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, query, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot insert vendor snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ type (
|
|||||||
ValidFrom *time.Time `db:"valid_from"`
|
ValidFrom *time.Time `db:"valid_from"`
|
||||||
ValidUntil *time.Time `db:"valid_until"`
|
ValidUntil *time.Time `db:"valid_until"`
|
||||||
FileID gid.GID `db:"file_id"`
|
FileID gid.GID `db:"file_id"`
|
||||||
SnapshotID *gid.GID `db:"snapshot_id"`
|
|
||||||
SourceID *gid.GID `db:"source_id"`
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}
|
}
|
||||||
@@ -84,8 +82,6 @@ SELECT
|
|||||||
valid_from,
|
valid_from,
|
||||||
valid_until,
|
valid_until,
|
||||||
file_id,
|
file_id,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -93,6 +89,7 @@ FROM
|
|||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND vendor_id = @vendor_id
|
AND vendor_id = @vendor_id
|
||||||
|
AND snapshot_id IS NULL
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -116,6 +113,60 @@ LIMIT 1;
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (vbaas *VendorBusinessAssociateAgreements) LoadByVendorIDs(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
scope Scoper,
|
||||||
|
vendorIDs []gid.GID,
|
||||||
|
) error {
|
||||||
|
if len(vendorIDs) == 0 {
|
||||||
|
*vbaas = VendorBusinessAssociateAgreements{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
vendor_id,
|
||||||
|
valid_from,
|
||||||
|
valid_until,
|
||||||
|
file_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
vendor_business_associate_agreements
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND vendor_id = ANY(@vendor_ids)
|
||||||
|
AND snapshot_id IS NULL
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
ids := make([]string, len(vendorIDs))
|
||||||
|
for i, id := range vendorIDs {
|
||||||
|
ids[i] = id.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
args := pgx.NamedArgs{"vendor_ids": ids}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query vendor business associate agreements: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
agreements, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[VendorBusinessAssociateAgreement])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect vendor business associate agreements: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*vbaas = agreements
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (vbaa *VendorBusinessAssociateAgreement) LoadByID(
|
func (vbaa *VendorBusinessAssociateAgreement) LoadByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Querier,
|
conn pg.Querier,
|
||||||
@@ -130,8 +181,6 @@ SELECT
|
|||||||
valid_from,
|
valid_from,
|
||||||
valid_until,
|
valid_until,
|
||||||
file_id,
|
file_id,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -215,8 +264,6 @@ INSERT INTO
|
|||||||
valid_from,
|
valid_from,
|
||||||
valid_until,
|
valid_until,
|
||||||
file_id,
|
file_id,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
)
|
)
|
||||||
@@ -228,8 +275,6 @@ VALUES (
|
|||||||
@valid_from,
|
@valid_from,
|
||||||
@valid_until,
|
@valid_until,
|
||||||
@file_id,
|
@file_id,
|
||||||
@snapshot_id,
|
|
||||||
@source_id,
|
|
||||||
@created_at,
|
@created_at,
|
||||||
@updated_at
|
@updated_at
|
||||||
)
|
)
|
||||||
@@ -238,8 +283,6 @@ ON CONFLICT (organization_id, vendor_id) DO UPDATE SET
|
|||||||
valid_from = EXCLUDED.valid_from,
|
valid_from = EXCLUDED.valid_from,
|
||||||
valid_until = EXCLUDED.valid_until,
|
valid_until = EXCLUDED.valid_until,
|
||||||
file_id = EXCLUDED.file_id,
|
file_id = EXCLUDED.file_id,
|
||||||
snapshot_id = EXCLUDED.snapshot_id,
|
|
||||||
source_id = EXCLUDED.source_id,
|
|
||||||
updated_at = EXCLUDED.updated_at
|
updated_at = EXCLUDED.updated_at
|
||||||
`
|
`
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
@@ -250,8 +293,6 @@ ON CONFLICT (organization_id, vendor_id) DO UPDATE SET
|
|||||||
"valid_from": vbaa.ValidFrom,
|
"valid_from": vbaa.ValidFrom,
|
||||||
"valid_until": vbaa.ValidUntil,
|
"valid_until": vbaa.ValidUntil,
|
||||||
"file_id": vbaa.FileID,
|
"file_id": vbaa.FileID,
|
||||||
"snapshot_id": vbaa.SnapshotID,
|
|
||||||
"source_id": vbaa.SourceID,
|
|
||||||
"created_at": vbaa.CreatedAt,
|
"created_at": vbaa.CreatedAt,
|
||||||
"updated_at": vbaa.UpdatedAt,
|
"updated_at": vbaa.UpdatedAt,
|
||||||
}
|
}
|
||||||
@@ -317,65 +358,3 @@ WHERE
|
|||||||
_, err := conn.Exec(ctx, q, args)
|
_, err := conn.Exec(ctx, q, args)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v VendorBusinessAssociateAgreements) InsertVendorSnapshots(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Tx,
|
|
||||||
scope Scoper,
|
|
||||||
organizationID gid.GID,
|
|
||||||
snapshotID gid.GID,
|
|
||||||
) error {
|
|
||||||
query := `
|
|
||||||
WITH
|
|
||||||
snapshot_vendors AS (
|
|
||||||
SELECT id, source_id
|
|
||||||
FROM vendors
|
|
||||||
WHERE organization_id = @organization_id AND snapshot_id = @snapshot_id
|
|
||||||
)
|
|
||||||
INSERT INTO vendor_business_associate_agreements (
|
|
||||||
tenant_id,
|
|
||||||
id,
|
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
organization_id,
|
|
||||||
vendor_id,
|
|
||||||
valid_from,
|
|
||||||
valid_until,
|
|
||||||
file_id,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
@tenant_id,
|
|
||||||
generate_gid(decode_base64_unpadded(@tenant_id), @vendor_business_associate_agreement_entity_type),
|
|
||||||
@snapshot_id,
|
|
||||||
vbaa.id,
|
|
||||||
vbaa.organization_id,
|
|
||||||
sv.id,
|
|
||||||
vbaa.valid_from,
|
|
||||||
vbaa.valid_until,
|
|
||||||
vbaa.file_id,
|
|
||||||
vbaa.created_at,
|
|
||||||
vbaa.updated_at
|
|
||||||
FROM vendor_business_associate_agreements vbaa
|
|
||||||
INNER JOIN snapshot_vendors sv ON sv.source_id = vbaa.vendor_id
|
|
||||||
WHERE %s AND vbaa.snapshot_id IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"tenant_id": scope.GetTenantID(),
|
|
||||||
"snapshot_id": snapshotID,
|
|
||||||
"organization_id": organizationID,
|
|
||||||
"vendor_business_associate_agreement_entity_type": VendorBusinessAssociateAgreementEntityType,
|
|
||||||
}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, query, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot insert vendor business associate agreement snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ type (
|
|||||||
ValidUntil *time.Time `db:"valid_until"`
|
ValidUntil *time.Time `db:"valid_until"`
|
||||||
ReportName string `db:"report_name"`
|
ReportName string `db:"report_name"`
|
||||||
ReportFileId *gid.GID `db:"report_file_id"`
|
ReportFileId *gid.GID `db:"report_file_id"`
|
||||||
SnapshotID *gid.GID `db:"snapshot_id"`
|
|
||||||
SourceID *gid.GID `db:"source_id"`
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}
|
}
|
||||||
@@ -86,8 +84,6 @@ SELECT
|
|||||||
valid_until,
|
valid_until,
|
||||||
report_name,
|
report_name,
|
||||||
report_file_id,
|
report_file_id,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -95,6 +91,7 @@ FROM
|
|||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND vendor_id = @vendor_id
|
AND vendor_id = @vendor_id
|
||||||
|
AND snapshot_id IS NULL
|
||||||
AND %s
|
AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -119,6 +116,63 @@ WHERE
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (vcs *VendorComplianceReports) LoadByVendorIDs(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
scope Scoper,
|
||||||
|
vendorIDs []gid.GID,
|
||||||
|
) error {
|
||||||
|
if len(vendorIDs) == 0 {
|
||||||
|
*vcs = VendorComplianceReports{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
vendor_id,
|
||||||
|
report_date,
|
||||||
|
valid_until,
|
||||||
|
report_name,
|
||||||
|
report_file_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
vendor_compliance_reports
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND vendor_id = ANY(@vendor_ids)
|
||||||
|
AND snapshot_id IS NULL
|
||||||
|
ORDER BY
|
||||||
|
vendor_id, report_date DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
ids := make([]string, len(vendorIDs))
|
||||||
|
for i, id := range vendorIDs {
|
||||||
|
ids[i] = id.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
args := pgx.NamedArgs{"vendor_ids": ids}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query vendor compliance reports: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vendorComplianceReports, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[VendorComplianceReport])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect vendor compliance reports: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*vcs = vendorComplianceReports
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (vcr *VendorComplianceReport) LoadByID(
|
func (vcr *VendorComplianceReport) LoadByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Querier,
|
conn pg.Querier,
|
||||||
@@ -134,8 +188,6 @@ SELECT
|
|||||||
valid_until,
|
valid_until,
|
||||||
report_name,
|
report_name,
|
||||||
report_file_id,
|
report_file_id,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -251,67 +303,3 @@ RETURNING report_file_id
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (vcrs VendorComplianceReports) InsertVendorSnapshots(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Tx,
|
|
||||||
scope Scoper,
|
|
||||||
organizationID gid.GID,
|
|
||||||
snapshotID gid.GID,
|
|
||||||
) error {
|
|
||||||
query := `
|
|
||||||
WITH
|
|
||||||
snapshot_vendors AS (
|
|
||||||
SELECT id, source_id
|
|
||||||
FROM vendors
|
|
||||||
WHERE organization_id = @organization_id AND snapshot_id = @snapshot_id
|
|
||||||
)
|
|
||||||
INSERT INTO vendor_compliance_reports (
|
|
||||||
tenant_id,
|
|
||||||
id,
|
|
||||||
organization_id,
|
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
vendor_id,
|
|
||||||
report_date,
|
|
||||||
valid_until,
|
|
||||||
report_name,
|
|
||||||
report_file_id,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
@tenant_id,
|
|
||||||
generate_gid(decode_base64_unpadded(@tenant_id), @vendor_compliance_report_entity_type),
|
|
||||||
@organization_id,
|
|
||||||
@snapshot_id,
|
|
||||||
vcr.id,
|
|
||||||
sv.id,
|
|
||||||
vcr.report_date,
|
|
||||||
vcr.valid_until,
|
|
||||||
vcr.report_name,
|
|
||||||
vcr.report_file_id,
|
|
||||||
vcr.created_at,
|
|
||||||
vcr.updated_at
|
|
||||||
FROM vendor_compliance_reports vcr
|
|
||||||
INNER JOIN snapshot_vendors sv ON sv.source_id = vcr.vendor_id
|
|
||||||
WHERE %s AND vcr.snapshot_id IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"tenant_id": scope.GetTenantID(),
|
|
||||||
"snapshot_id": snapshotID,
|
|
||||||
"organization_id": organizationID,
|
|
||||||
"vendor_compliance_report_entity_type": VendorComplianceReportEntityType,
|
|
||||||
}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, query, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot insert vendor compliance report snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ type (
|
|||||||
Email *mail.Addr `db:"email"`
|
Email *mail.Addr `db:"email"`
|
||||||
Phone *string `db:"phone"`
|
Phone *string `db:"phone"`
|
||||||
Role *string `db:"role"`
|
Role *string `db:"role"`
|
||||||
SnapshotID *gid.GID `db:"snapshot_id"`
|
|
||||||
SourceID *gid.GID `db:"source_id"`
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}
|
}
|
||||||
@@ -88,8 +86,6 @@ SELECT
|
|||||||
email,
|
email,
|
||||||
phone,
|
phone,
|
||||||
role,
|
role,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -141,8 +137,6 @@ SELECT
|
|||||||
email,
|
email,
|
||||||
phone,
|
phone,
|
||||||
role,
|
role,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -150,6 +144,7 @@ FROM
|
|||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND vendor_id = @vendor_id
|
AND vendor_id = @vendor_id
|
||||||
|
AND snapshot_id IS NULL
|
||||||
AND %s
|
AND %s
|
||||||
`
|
`
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||||
@@ -176,6 +171,63 @@ WHERE
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (vc *VendorContacts) LoadByVendorIDs(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
scope Scoper,
|
||||||
|
vendorIDs []gid.GID,
|
||||||
|
) error {
|
||||||
|
if len(vendorIDs) == 0 {
|
||||||
|
*vc = VendorContacts{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
vendor_id,
|
||||||
|
full_name,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
role,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
vendor_contacts
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND vendor_id = ANY(@vendor_ids)
|
||||||
|
AND snapshot_id IS NULL
|
||||||
|
ORDER BY
|
||||||
|
vendor_id, full_name ASC
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
ids := make([]string, len(vendorIDs))
|
||||||
|
for i, id := range vendorIDs {
|
||||||
|
ids[i] = id.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"vendor_ids": ids}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query vendor contacts: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
vendorContacts, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[VendorContact])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect vendor contacts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*vc = vendorContacts
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (vc VendorContact) Insert(
|
func (vc VendorContact) Insert(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Tx,
|
conn pg.Tx,
|
||||||
@@ -296,67 +348,3 @@ WHERE
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (vc VendorContacts) InsertVendorSnapshots(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Tx,
|
|
||||||
scope Scoper,
|
|
||||||
organizationID gid.GID,
|
|
||||||
snapshotID gid.GID,
|
|
||||||
) error {
|
|
||||||
query := `
|
|
||||||
WITH
|
|
||||||
snapshot_vendors AS (
|
|
||||||
SELECT id, source_id
|
|
||||||
FROM vendors
|
|
||||||
WHERE organization_id = @organization_id AND snapshot_id = @snapshot_id
|
|
||||||
)
|
|
||||||
INSERT INTO vendor_contacts (
|
|
||||||
tenant_id,
|
|
||||||
id,
|
|
||||||
organization_id,
|
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
vendor_id,
|
|
||||||
full_name,
|
|
||||||
email,
|
|
||||||
phone,
|
|
||||||
role,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
@tenant_id,
|
|
||||||
generate_gid(decode_base64_unpadded(@tenant_id), @vendor_contact_entity_type),
|
|
||||||
@organization_id,
|
|
||||||
@snapshot_id,
|
|
||||||
vc.id,
|
|
||||||
sv.id,
|
|
||||||
vc.full_name,
|
|
||||||
vc.email,
|
|
||||||
vc.phone,
|
|
||||||
vc.role,
|
|
||||||
vc.created_at,
|
|
||||||
vc.updated_at
|
|
||||||
FROM vendor_contacts vc
|
|
||||||
INNER JOIN snapshot_vendors sv ON sv.source_id = vc.vendor_id
|
|
||||||
WHERE %s AND vc.snapshot_id IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"tenant_id": scope.GetTenantID(),
|
|
||||||
"snapshot_id": snapshotID,
|
|
||||||
"organization_id": organizationID,
|
|
||||||
"vendor_contact_entity_type": VendorContactEntityType,
|
|
||||||
}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, query, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot insert vendor contact snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ type (
|
|||||||
ValidFrom *time.Time `db:"valid_from"`
|
ValidFrom *time.Time `db:"valid_from"`
|
||||||
ValidUntil *time.Time `db:"valid_until"`
|
ValidUntil *time.Time `db:"valid_until"`
|
||||||
FileID gid.GID `db:"file_id"`
|
FileID gid.GID `db:"file_id"`
|
||||||
SnapshotID *gid.GID `db:"snapshot_id"`
|
|
||||||
SourceID *gid.GID `db:"source_id"`
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}
|
}
|
||||||
@@ -84,8 +82,6 @@ SELECT
|
|||||||
valid_from,
|
valid_from,
|
||||||
valid_until,
|
valid_until,
|
||||||
file_id,
|
file_id,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -93,6 +89,7 @@ FROM
|
|||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND vendor_id = @vendor_id
|
AND vendor_id = @vendor_id
|
||||||
|
AND snapshot_id IS NULL
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -116,6 +113,60 @@ LIMIT 1;
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (vdpas *VendorDataPrivacyAgreements) LoadByVendorIDs(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
scope Scoper,
|
||||||
|
vendorIDs []gid.GID,
|
||||||
|
) error {
|
||||||
|
if len(vendorIDs) == 0 {
|
||||||
|
*vdpas = VendorDataPrivacyAgreements{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
vendor_id,
|
||||||
|
valid_from,
|
||||||
|
valid_until,
|
||||||
|
file_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
vendor_data_privacy_agreements
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND vendor_id = ANY(@vendor_ids)
|
||||||
|
AND snapshot_id IS NULL
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
ids := make([]string, len(vendorIDs))
|
||||||
|
for i, id := range vendorIDs {
|
||||||
|
ids[i] = id.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
args := pgx.NamedArgs{"vendor_ids": ids}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query vendor data privacy agreements: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
agreements, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[VendorDataPrivacyAgreement])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect vendor data privacy agreements: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*vdpas = agreements
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (vdpa *VendorDataPrivacyAgreement) LoadByID(
|
func (vdpa *VendorDataPrivacyAgreement) LoadByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Querier,
|
conn pg.Querier,
|
||||||
@@ -130,8 +181,6 @@ SELECT
|
|||||||
valid_from,
|
valid_from,
|
||||||
valid_until,
|
valid_until,
|
||||||
file_id,
|
file_id,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -215,8 +264,6 @@ INSERT INTO
|
|||||||
valid_from,
|
valid_from,
|
||||||
valid_until,
|
valid_until,
|
||||||
file_id,
|
file_id,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
)
|
)
|
||||||
@@ -228,8 +275,6 @@ VALUES (
|
|||||||
@valid_from,
|
@valid_from,
|
||||||
@valid_until,
|
@valid_until,
|
||||||
@file_id,
|
@file_id,
|
||||||
@snapshot_id,
|
|
||||||
@source_id,
|
|
||||||
@created_at,
|
@created_at,
|
||||||
@updated_at
|
@updated_at
|
||||||
)
|
)
|
||||||
@@ -238,8 +283,6 @@ ON CONFLICT (organization_id, vendor_id) DO UPDATE SET
|
|||||||
valid_from = EXCLUDED.valid_from,
|
valid_from = EXCLUDED.valid_from,
|
||||||
valid_until = EXCLUDED.valid_until,
|
valid_until = EXCLUDED.valid_until,
|
||||||
file_id = EXCLUDED.file_id,
|
file_id = EXCLUDED.file_id,
|
||||||
snapshot_id = EXCLUDED.snapshot_id,
|
|
||||||
source_id = EXCLUDED.source_id,
|
|
||||||
updated_at = EXCLUDED.updated_at
|
updated_at = EXCLUDED.updated_at
|
||||||
`
|
`
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
@@ -250,8 +293,6 @@ ON CONFLICT (organization_id, vendor_id) DO UPDATE SET
|
|||||||
"valid_from": vdpa.ValidFrom,
|
"valid_from": vdpa.ValidFrom,
|
||||||
"valid_until": vdpa.ValidUntil,
|
"valid_until": vdpa.ValidUntil,
|
||||||
"file_id": vdpa.FileID,
|
"file_id": vdpa.FileID,
|
||||||
"snapshot_id": vdpa.SnapshotID,
|
|
||||||
"source_id": vdpa.SourceID,
|
|
||||||
"created_at": vdpa.CreatedAt,
|
"created_at": vdpa.CreatedAt,
|
||||||
"updated_at": vdpa.UpdatedAt,
|
"updated_at": vdpa.UpdatedAt,
|
||||||
}
|
}
|
||||||
@@ -316,65 +357,3 @@ WHERE
|
|||||||
_, err := conn.Exec(ctx, q, args)
|
_, err := conn.Exec(ctx, q, args)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (vdpa VendorDataPrivacyAgreements) InsertVendorSnapshots(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Tx,
|
|
||||||
scope Scoper,
|
|
||||||
organizationID gid.GID,
|
|
||||||
snapshotID gid.GID,
|
|
||||||
) error {
|
|
||||||
query := `
|
|
||||||
WITH
|
|
||||||
snapshot_vendors AS (
|
|
||||||
SELECT id, source_id
|
|
||||||
FROM vendors
|
|
||||||
WHERE organization_id = @organization_id AND snapshot_id = @snapshot_id
|
|
||||||
)
|
|
||||||
INSERT INTO vendor_data_privacy_agreements (
|
|
||||||
tenant_id,
|
|
||||||
id,
|
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
organization_id,
|
|
||||||
vendor_id,
|
|
||||||
valid_from,
|
|
||||||
valid_until,
|
|
||||||
file_id,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
@tenant_id,
|
|
||||||
generate_gid(decode_base64_unpadded(@tenant_id), @vendor_data_privacy_agreement_entity_type),
|
|
||||||
@snapshot_id,
|
|
||||||
vdpa.id,
|
|
||||||
vdpa.organization_id,
|
|
||||||
sv.id,
|
|
||||||
vdpa.valid_from,
|
|
||||||
vdpa.valid_until,
|
|
||||||
vdpa.file_id,
|
|
||||||
vdpa.created_at,
|
|
||||||
vdpa.updated_at
|
|
||||||
FROM vendor_data_privacy_agreements vdpa
|
|
||||||
INNER JOIN snapshot_vendors sv ON sv.source_id = vdpa.vendor_id
|
|
||||||
WHERE %s AND vdpa.snapshot_id IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"tenant_id": scope.GetTenantID(),
|
|
||||||
"snapshot_id": snapshotID,
|
|
||||||
"organization_id": organizationID,
|
|
||||||
"vendor_data_privacy_agreement_entity_type": VendorDataPrivacyAgreementEntityType,
|
|
||||||
}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, query, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot insert vendor data privacy agreement snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,19 +16,16 @@ package coredata
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
VendorFilter struct {
|
VendorFilter struct {
|
||||||
showOnTrustCenter *bool
|
showOnTrustCenter *bool
|
||||||
snapshotID **gid.GID
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewVendorFilter(snapshotID **gid.GID, showOnTrustCenter *bool) *VendorFilter {
|
func NewVendorFilter(showOnTrustCenter *bool) *VendorFilter {
|
||||||
return &VendorFilter{
|
return &VendorFilter{
|
||||||
snapshotID: snapshotID,
|
|
||||||
showOnTrustCenter: showOnTrustCenter,
|
showOnTrustCenter: showOnTrustCenter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,17 +39,6 @@ func (f *VendorFilter) SQLArguments() pgx.StrictNamedArgs {
|
|||||||
args["show_on_trust_center"] = nil
|
args["show_on_trust_center"] = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.snapshotID == nil {
|
|
||||||
args["has_snapshot_filter"] = false
|
|
||||||
args["filter_snapshot_id"] = nil
|
|
||||||
} else if *f.snapshotID == nil {
|
|
||||||
args["has_snapshot_filter"] = true
|
|
||||||
args["filter_snapshot_id"] = nil
|
|
||||||
} else {
|
|
||||||
args["has_snapshot_filter"] = true
|
|
||||||
args["filter_snapshot_id"] = **f.snapshotID
|
|
||||||
}
|
|
||||||
|
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,14 +50,5 @@ func (f *VendorFilter) SQLFragment() string {
|
|||||||
show_on_trust_center = @show_on_trust_center::boolean
|
show_on_trust_center = @show_on_trust_center::boolean
|
||||||
ELSE TRUE
|
ELSE TRUE
|
||||||
END
|
END
|
||||||
AND
|
|
||||||
CASE
|
|
||||||
WHEN @has_snapshot_filter::boolean = false THEN TRUE
|
|
||||||
WHEN @has_snapshot_filter::boolean = true AND @filter_snapshot_id::text IS NOT NULL THEN
|
|
||||||
snapshot_id = @filter_snapshot_id::text
|
|
||||||
WHEN @has_snapshot_filter::boolean = true AND @filter_snapshot_id::text IS NULL THEN
|
|
||||||
snapshot_id IS NULL
|
|
||||||
ELSE TRUE
|
|
||||||
END
|
|
||||||
)`
|
)`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ type (
|
|||||||
DataSensitivity DataSensitivity `db:"data_sensitivity"`
|
DataSensitivity DataSensitivity `db:"data_sensitivity"`
|
||||||
BusinessImpact BusinessImpact `db:"business_impact"`
|
BusinessImpact BusinessImpact `db:"business_impact"`
|
||||||
Notes *string `db:"notes"`
|
Notes *string `db:"notes"`
|
||||||
SnapshotID *gid.GID `db:"snapshot_id"`
|
|
||||||
SourceID *gid.GID `db:"source_id"`
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}
|
}
|
||||||
@@ -137,8 +135,6 @@ SELECT
|
|||||||
data_sensitivity,
|
data_sensitivity,
|
||||||
business_impact,
|
business_impact,
|
||||||
notes,
|
notes,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -186,8 +182,6 @@ SELECT
|
|||||||
data_sensitivity,
|
data_sensitivity,
|
||||||
business_impact,
|
business_impact,
|
||||||
notes,
|
notes,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -195,6 +189,7 @@ FROM
|
|||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND vendor_id = @vendor_id
|
AND vendor_id = @vendor_id
|
||||||
|
AND snapshot_id IS NULL
|
||||||
ORDER BY
|
ORDER BY
|
||||||
created_at DESC
|
created_at DESC
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
@@ -238,8 +233,6 @@ SELECT
|
|||||||
data_sensitivity,
|
data_sensitivity,
|
||||||
business_impact,
|
business_impact,
|
||||||
notes,
|
notes,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -247,6 +240,7 @@ FROM
|
|||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND vendor_id = @vendor_id
|
AND vendor_id = @vendor_id
|
||||||
|
AND snapshot_id IS NULL
|
||||||
AND %s
|
AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -271,25 +265,20 @@ WHERE
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v VendorRiskAssessments) InsertVendorSnapshots(
|
func (r *VendorRiskAssessments) LoadByVendorIDs(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Tx,
|
conn pg.Querier,
|
||||||
scope Scoper,
|
scope Scoper,
|
||||||
organizationID gid.GID,
|
vendorIDs []gid.GID,
|
||||||
snapshotID gid.GID,
|
|
||||||
) error {
|
) error {
|
||||||
query := `
|
if len(vendorIDs) == 0 {
|
||||||
WITH
|
*r = VendorRiskAssessments{}
|
||||||
snapshot_vendors AS (
|
return nil
|
||||||
SELECT id, source_id
|
}
|
||||||
FROM vendors
|
|
||||||
WHERE organization_id = @organization_id AND snapshot_id = @snapshot_id
|
q := `
|
||||||
)
|
SELECT
|
||||||
INSERT INTO vendor_risk_assessments (
|
|
||||||
tenant_id,
|
|
||||||
id,
|
id,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
organization_id,
|
organization_id,
|
||||||
vendor_id,
|
vendor_id,
|
||||||
expires_at,
|
expires_at,
|
||||||
@@ -298,39 +287,37 @@ INSERT INTO vendor_risk_assessments (
|
|||||||
notes,
|
notes,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
)
|
FROM
|
||||||
SELECT
|
vendor_risk_assessments
|
||||||
@tenant_id,
|
WHERE
|
||||||
generate_gid(decode_base64_unpadded(@tenant_id), @vendor_risk_assessment_entity_type),
|
%s
|
||||||
@snapshot_id,
|
AND vendor_id = ANY(@vendor_ids)
|
||||||
vra.id,
|
AND snapshot_id IS NULL
|
||||||
vra.organization_id,
|
ORDER BY
|
||||||
sv.id,
|
vendor_id, created_at DESC
|
||||||
vra.expires_at,
|
|
||||||
vra.data_sensitivity,
|
|
||||||
vra.business_impact,
|
|
||||||
vra.notes,
|
|
||||||
vra.created_at,
|
|
||||||
vra.updated_at
|
|
||||||
FROM vendor_risk_assessments vra
|
|
||||||
INNER JOIN snapshot_vendors sv ON sv.source_id = vra.vendor_id
|
|
||||||
WHERE %s AND vra.snapshot_id IS NULL
|
|
||||||
`
|
`
|
||||||
|
|
||||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
ids := make([]string, len(vendorIDs))
|
||||||
"tenant_id": scope.GetTenantID(),
|
for i, id := range vendorIDs {
|
||||||
"snapshot_id": snapshotID,
|
ids[i] = id.String()
|
||||||
"organization_id": organizationID,
|
|
||||||
"vendor_risk_assessment_entity_type": VendorRiskAssessmentEntityType,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"vendor_ids": ids}
|
||||||
maps.Copy(args, scope.SQLArguments())
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, query, args)
|
rows, err := conn.Query(ctx, q, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot insert vendor risk assessment snapshots: %w", err)
|
return fmt.Errorf("cannot query risk assessments: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assessments, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[VendorRiskAssessment])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect risk assessments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*r = assessments
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ type (
|
|||||||
VendorID gid.GID `db:"vendor_id"`
|
VendorID gid.GID `db:"vendor_id"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
Description *string `db:"description"`
|
Description *string `db:"description"`
|
||||||
SnapshotID *gid.GID `db:"snapshot_id"`
|
|
||||||
SourceID *gid.GID `db:"source_id"`
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}
|
}
|
||||||
@@ -81,8 +79,6 @@ SELECT
|
|||||||
vendor_id,
|
vendor_id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -132,8 +128,6 @@ SELECT
|
|||||||
vendor_id,
|
vendor_id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -141,6 +135,7 @@ FROM
|
|||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND vendor_id = @vendor_id
|
AND vendor_id = @vendor_id
|
||||||
|
AND snapshot_id IS NULL
|
||||||
AND %s
|
AND %s
|
||||||
`
|
`
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||||
@@ -167,6 +162,61 @@ WHERE
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (vs *VendorServices) LoadByVendorIDs(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
scope Scoper,
|
||||||
|
vendorIDs []gid.GID,
|
||||||
|
) error {
|
||||||
|
if len(vendorIDs) == 0 {
|
||||||
|
*vs = VendorServices{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
vendor_id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
vendor_services
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND vendor_id = ANY(@vendor_ids)
|
||||||
|
AND snapshot_id IS NULL
|
||||||
|
ORDER BY
|
||||||
|
vendor_id, name ASC
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
ids := make([]string, len(vendorIDs))
|
||||||
|
for i, id := range vendorIDs {
|
||||||
|
ids[i] = id.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"vendor_ids": ids}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query vendor services: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
vendorServices, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[VendorService])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect vendor services: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*vs = vendorServices
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (vs VendorService) Insert(
|
func (vs VendorService) Insert(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Tx,
|
conn pg.Tx,
|
||||||
@@ -277,63 +327,3 @@ WHERE
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (vs VendorServices) InsertVendorSnapshots(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Tx,
|
|
||||||
scope Scoper,
|
|
||||||
organizationID gid.GID,
|
|
||||||
snapshotID gid.GID,
|
|
||||||
) error {
|
|
||||||
query := `
|
|
||||||
WITH
|
|
||||||
snapshot_vendors AS (
|
|
||||||
SELECT id, source_id
|
|
||||||
FROM vendors
|
|
||||||
WHERE organization_id = @organization_id AND snapshot_id = @snapshot_id
|
|
||||||
)
|
|
||||||
INSERT INTO vendor_services (
|
|
||||||
tenant_id,
|
|
||||||
id,
|
|
||||||
organization_id,
|
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
vendor_id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
@tenant_id,
|
|
||||||
generate_gid(decode_base64_unpadded(@tenant_id), @vendor_service_entity_type),
|
|
||||||
@organization_id,
|
|
||||||
@snapshot_id,
|
|
||||||
vs.id,
|
|
||||||
sv.id,
|
|
||||||
vs.name,
|
|
||||||
vs.description,
|
|
||||||
vs.created_at,
|
|
||||||
vs.updated_at
|
|
||||||
FROM vendor_services vs
|
|
||||||
INNER JOIN snapshot_vendors sv ON sv.source_id = vs.vendor_id
|
|
||||||
WHERE %s AND vs.snapshot_id IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"tenant_id": scope.GetTenantID(),
|
|
||||||
"snapshot_id": snapshotID,
|
|
||||||
"organization_id": organizationID,
|
|
||||||
"vendor_service_entity_type": VendorServiceEntityType,
|
|
||||||
}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, query, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot insert vendor service snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -366,6 +366,73 @@ type (
|
|||||||
LocalLawRisk string
|
LocalLawRisk string
|
||||||
SupplementaryMeasures string
|
SupplementaryMeasures string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VendorListData struct {
|
||||||
|
Title string
|
||||||
|
OrganizationName string
|
||||||
|
CreatedAt time.Time
|
||||||
|
TotalVendors int
|
||||||
|
Rows []VendorListRow
|
||||||
|
}
|
||||||
|
|
||||||
|
VendorListRow struct {
|
||||||
|
Name string
|
||||||
|
LegalName string
|
||||||
|
Description string
|
||||||
|
Category string
|
||||||
|
HeadquarterAddress string
|
||||||
|
WebsiteURL string
|
||||||
|
PrivacyPolicyURL string
|
||||||
|
ServiceLevelAgreementURL string
|
||||||
|
DataProcessingAgreementURL string
|
||||||
|
BusinessAssociateAgreementURL string
|
||||||
|
SubprocessorsListURL string
|
||||||
|
StatusPageURL string
|
||||||
|
TermsOfServiceURL string
|
||||||
|
SecurityPageURL string
|
||||||
|
TrustPageURL string
|
||||||
|
Certifications string
|
||||||
|
Countries string
|
||||||
|
BusinessOwner string
|
||||||
|
SecurityOwner string
|
||||||
|
Services []VendorListService
|
||||||
|
Contacts []VendorListContact
|
||||||
|
RiskAssessments []VendorListRiskAssessment
|
||||||
|
ComplianceReports []VendorListComplianceReport
|
||||||
|
BusinessAssociateAgreement *VendorListAgreement
|
||||||
|
DataPrivacyAgreement *VendorListAgreement
|
||||||
|
}
|
||||||
|
|
||||||
|
VendorListService struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
VendorListContact struct {
|
||||||
|
FullName string
|
||||||
|
Email string
|
||||||
|
Phone string
|
||||||
|
Role string
|
||||||
|
}
|
||||||
|
|
||||||
|
VendorListRiskAssessment struct {
|
||||||
|
AssessedAt string
|
||||||
|
ExpiresAt string
|
||||||
|
DataSensitivity string
|
||||||
|
BusinessImpact string
|
||||||
|
Notes string
|
||||||
|
}
|
||||||
|
|
||||||
|
VendorListComplianceReport struct {
|
||||||
|
ReportName string
|
||||||
|
ReportDate string
|
||||||
|
ValidUntil string
|
||||||
|
}
|
||||||
|
|
||||||
|
VendorListAgreement struct {
|
||||||
|
ValidFrom string
|
||||||
|
ValidUntil string
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func BoolLabel(v bool) string {
|
func BoolLabel(v bool) string {
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ const (
|
|||||||
ActionVendorUpdate = "core:vendor:update"
|
ActionVendorUpdate = "core:vendor:update"
|
||||||
ActionVendorDelete = "core:vendor:delete"
|
ActionVendorDelete = "core:vendor:delete"
|
||||||
ActionVendorAssess = "core:vendor:assess"
|
ActionVendorAssess = "core:vendor:assess"
|
||||||
|
ActionVendorPublish = "core:vendor:publish"
|
||||||
|
|
||||||
// VendorContact actions
|
// VendorContact actions
|
||||||
ActionVendorContactGet = "core:vendor-contact:get"
|
ActionVendorContactGet = "core:vendor-contact:get"
|
||||||
|
|||||||
@@ -2563,3 +2563,536 @@ func BuildTransferImpactAssessmentListDocument(data docgen.TransferImpactAssessm
|
|||||||
}
|
}
|
||||||
return buf.String(), nil
|
return buf.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *GeneratedDocumentService) PublishVendorList(
|
||||||
|
ctx context.Context,
|
||||||
|
organizationID gid.GID,
|
||||||
|
approverIDs []gid.GID,
|
||||||
|
) (*coredata.Document, *coredata.DocumentVersion, error) {
|
||||||
|
// Phase 1: collect data and render the prosemirror document outside any
|
||||||
|
// write transaction. Both the bulk reads of vendors + sub-entities and the
|
||||||
|
// JSON template rendering are slow enough that holding write locks across
|
||||||
|
// them would needlessly block other writers.
|
||||||
|
var documentData docgen.VendorListData
|
||||||
|
err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
||||||
|
organization := &coredata.Organization{}
|
||||||
|
if err := organization.LoadByID(ctx, conn, s.svc.scope, organizationID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
documentData, err = s.buildVendorListDocumentData(ctx, conn, organization)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot build document data: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prosemirrorJSON, err := BuildVendorListDocument(documentData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("cannot build prosemirror document: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: persist the document and version in a write transaction.
|
||||||
|
var (
|
||||||
|
document *coredata.Document
|
||||||
|
documentVersion *coredata.DocumentVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
err = s.svc.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(ctx context.Context, tx pg.Tx) error {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
vendor := coredata.Vendor{}
|
||||||
|
vendorDocumentID, err := vendor.GetGeneratedDocumentID(ctx, tx, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query generated documents: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingDoc *coredata.Document
|
||||||
|
if vendorDocumentID != nil {
|
||||||
|
doc := &coredata.Document{}
|
||||||
|
err = doc.LoadByID(ctx, tx, s.svc.scope, *vendorDocumentID)
|
||||||
|
if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return fmt.Errorf("cannot load vendor list document: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil && doc.ArchivedAt == nil {
|
||||||
|
existingDoc = doc
|
||||||
|
} else {
|
||||||
|
if err := vendor.ClearGeneratedDocumentID(ctx, tx, []gid.GID{*vendorDocumentID}); err != nil {
|
||||||
|
return fmt.Errorf("cannot clear document reference: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasApprovers := len(approverIDs) > 0
|
||||||
|
|
||||||
|
if existingDoc == nil {
|
||||||
|
documentID := gid.New(s.svc.scope.GetTenantID(), coredata.DocumentEntityType)
|
||||||
|
|
||||||
|
document = &coredata.Document{
|
||||||
|
ID: documentID,
|
||||||
|
OrganizationID: organizationID,
|
||||||
|
WriteMode: coredata.DocumentWriteModeGenerated,
|
||||||
|
TrustCenterVisibility: coredata.TrustCenterVisibilityNone,
|
||||||
|
Status: coredata.DocumentStatusActive,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := document.Insert(ctx, tx, s.svc.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot insert document: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := vendor.UpsertGeneratedDocumentID(ctx, tx, organizationID, s.svc.scope.GetTenantID(), documentID); err != nil {
|
||||||
|
return fmt.Errorf("cannot upsert generated documents: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document = existingDoc
|
||||||
|
}
|
||||||
|
|
||||||
|
var newMajor int
|
||||||
|
if document.CurrentPublishedMajor != nil {
|
||||||
|
newMajor = *document.CurrentPublishedMajor + 1
|
||||||
|
} else {
|
||||||
|
newMajor = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
versionStatus := coredata.DocumentVersionStatusPublished
|
||||||
|
var publishedAt *time.Time
|
||||||
|
if hasApprovers {
|
||||||
|
versionStatus = coredata.DocumentVersionStatusDraft
|
||||||
|
} else {
|
||||||
|
publishedAt = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
documentVersionID := gid.New(s.svc.scope.GetTenantID(), coredata.DocumentVersionEntityType)
|
||||||
|
documentVersion = &coredata.DocumentVersion{
|
||||||
|
ID: documentVersionID,
|
||||||
|
OrganizationID: organizationID,
|
||||||
|
DocumentID: document.ID,
|
||||||
|
Title: "Vendors",
|
||||||
|
Major: newMajor,
|
||||||
|
Minor: 0,
|
||||||
|
Content: prosemirrorJSON,
|
||||||
|
Status: versionStatus,
|
||||||
|
Classification: coredata.DocumentClassificationConfidential,
|
||||||
|
DocumentType: coredata.DocumentTypeRegister,
|
||||||
|
Orientation: coredata.DocumentVersionOrientationPortrait,
|
||||||
|
PublishedAt: publishedAt,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := documentVersion.Insert(ctx, tx, s.svc.scope); err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
||||||
|
return fmt.Errorf("a version is pending approval, approve or reject it before publishing a new one: %w", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot insert document version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasApprovers {
|
||||||
|
defaultApprovers := &coredata.DocumentDefaultApprovers{}
|
||||||
|
if err := defaultApprovers.MergeByDocumentID(ctx, tx, s.svc.scope, document.ID, organizationID, approverIDs); err != nil {
|
||||||
|
return fmt.Errorf("cannot save default approvers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.svc.DocumentApprovals.RequestApprovalInTx(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
document,
|
||||||
|
documentVersion,
|
||||||
|
approverIDs,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot request approval: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
zero := 0
|
||||||
|
document.CurrentPublishedMajor = &newMajor
|
||||||
|
document.CurrentPublishedMinor = &zero
|
||||||
|
document.UpdatedAt = now
|
||||||
|
|
||||||
|
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot update document: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return document, documentVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GeneratedDocumentService) GetVendorsDocumentID(
|
||||||
|
ctx context.Context,
|
||||||
|
organizationID gid.GID,
|
||||||
|
) (*gid.GID, error) {
|
||||||
|
var documentID *gid.GID
|
||||||
|
|
||||||
|
err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
||||||
|
vendor := coredata.Vendor{}
|
||||||
|
var err error
|
||||||
|
documentID, err = vendor.GetGeneratedDocumentID(ctx, conn, organizationID)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot get vendor list document ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return documentID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GeneratedDocumentService) buildVendorListDocumentData(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
organization *coredata.Organization,
|
||||||
|
) (docgen.VendorListData, error) {
|
||||||
|
var vendors coredata.Vendors
|
||||||
|
if err := vendors.LoadAllByOrganizationID(ctx, conn, s.svc.scope, organization.ID); err != nil {
|
||||||
|
return docgen.VendorListData{}, fmt.Errorf("cannot load vendors: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(vendors) == 0 {
|
||||||
|
return docgen.VendorListData{
|
||||||
|
Title: "Vendors",
|
||||||
|
OrganizationName: organization.Name,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
TotalVendors: 0,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ownerIDSet := make(map[gid.GID]struct{})
|
||||||
|
ownerIDs := make([]gid.GID, 0)
|
||||||
|
for _, v := range vendors {
|
||||||
|
if v.BusinessOwnerID != nil {
|
||||||
|
if _, ok := ownerIDSet[*v.BusinessOwnerID]; !ok {
|
||||||
|
ownerIDs = append(ownerIDs, *v.BusinessOwnerID)
|
||||||
|
ownerIDSet[*v.BusinessOwnerID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v.SecurityOwnerID != nil {
|
||||||
|
if _, ok := ownerIDSet[*v.SecurityOwnerID]; !ok {
|
||||||
|
ownerIDs = append(ownerIDs, *v.SecurityOwnerID)
|
||||||
|
ownerIDSet[*v.SecurityOwnerID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
profileMap := make(map[gid.GID]*coredata.MembershipProfile)
|
||||||
|
if len(ownerIDs) > 0 {
|
||||||
|
var profiles coredata.MembershipProfiles
|
||||||
|
if err := profiles.LoadByIDs(ctx, conn, s.svc.scope, ownerIDs); err != nil {
|
||||||
|
return docgen.VendorListData{}, fmt.Errorf("cannot load owner profiles: %w", err)
|
||||||
|
}
|
||||||
|
for _, p := range profiles {
|
||||||
|
profileMap[p.ID] = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vendorIDs := make([]gid.GID, len(vendors))
|
||||||
|
for i, v := range vendors {
|
||||||
|
vendorIDs[i] = v.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
var allServices coredata.VendorServices
|
||||||
|
if err := allServices.LoadByVendorIDs(ctx, conn, s.svc.scope, vendorIDs); err != nil {
|
||||||
|
return docgen.VendorListData{}, fmt.Errorf("cannot load vendor services: %w", err)
|
||||||
|
}
|
||||||
|
servicesByVendor := make(map[gid.GID]coredata.VendorServices, len(vendors))
|
||||||
|
for _, vs := range allServices {
|
||||||
|
servicesByVendor[vs.VendorID] = append(servicesByVendor[vs.VendorID], vs)
|
||||||
|
}
|
||||||
|
|
||||||
|
var allContacts coredata.VendorContacts
|
||||||
|
if err := allContacts.LoadByVendorIDs(ctx, conn, s.svc.scope, vendorIDs); err != nil {
|
||||||
|
return docgen.VendorListData{}, fmt.Errorf("cannot load vendor contacts: %w", err)
|
||||||
|
}
|
||||||
|
contactsByVendor := make(map[gid.GID]coredata.VendorContacts, len(vendors))
|
||||||
|
for _, c := range allContacts {
|
||||||
|
contactsByVendor[c.VendorID] = append(contactsByVendor[c.VendorID], c)
|
||||||
|
}
|
||||||
|
|
||||||
|
var allAssessments coredata.VendorRiskAssessments
|
||||||
|
if err := allAssessments.LoadByVendorIDs(ctx, conn, s.svc.scope, vendorIDs); err != nil {
|
||||||
|
return docgen.VendorListData{}, fmt.Errorf("cannot load vendor risk assessments: %w", err)
|
||||||
|
}
|
||||||
|
assessmentsByVendor := make(map[gid.GID]coredata.VendorRiskAssessments, len(vendors))
|
||||||
|
for _, ra := range allAssessments {
|
||||||
|
assessmentsByVendor[ra.VendorID] = append(assessmentsByVendor[ra.VendorID], ra)
|
||||||
|
}
|
||||||
|
|
||||||
|
var allReports coredata.VendorComplianceReports
|
||||||
|
if err := allReports.LoadByVendorIDs(ctx, conn, s.svc.scope, vendorIDs); err != nil {
|
||||||
|
return docgen.VendorListData{}, fmt.Errorf("cannot load vendor compliance reports: %w", err)
|
||||||
|
}
|
||||||
|
reportsByVendor := make(map[gid.GID]coredata.VendorComplianceReports, len(vendors))
|
||||||
|
for _, r := range allReports {
|
||||||
|
reportsByVendor[r.VendorID] = append(reportsByVendor[r.VendorID], r)
|
||||||
|
}
|
||||||
|
|
||||||
|
var allBAAs coredata.VendorBusinessAssociateAgreements
|
||||||
|
if err := allBAAs.LoadByVendorIDs(ctx, conn, s.svc.scope, vendorIDs); err != nil {
|
||||||
|
return docgen.VendorListData{}, fmt.Errorf("cannot load vendor business associate agreements: %w", err)
|
||||||
|
}
|
||||||
|
baaByVendor := make(map[gid.GID]*coredata.VendorBusinessAssociateAgreement, len(allBAAs))
|
||||||
|
for _, b := range allBAAs {
|
||||||
|
baaByVendor[b.VendorID] = b
|
||||||
|
}
|
||||||
|
|
||||||
|
var allDPAs coredata.VendorDataPrivacyAgreements
|
||||||
|
if err := allDPAs.LoadByVendorIDs(ctx, conn, s.svc.scope, vendorIDs); err != nil {
|
||||||
|
return docgen.VendorListData{}, fmt.Errorf("cannot load vendor data privacy agreements: %w", err)
|
||||||
|
}
|
||||||
|
dpaByVendor := make(map[gid.GID]*coredata.VendorDataPrivacyAgreement, len(allDPAs))
|
||||||
|
for _, d := range allDPAs {
|
||||||
|
dpaByVendor[d.VendorID] = d
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]docgen.VendorListRow, 0, len(vendors))
|
||||||
|
for _, v := range vendors {
|
||||||
|
row := docgen.VendorListRow{
|
||||||
|
Name: v.Name,
|
||||||
|
LegalName: derefStringOrNotSpecified(v.LegalName),
|
||||||
|
Description: derefStringOrNotSpecified(v.Description),
|
||||||
|
Category: formatVendorCategory(v.Category),
|
||||||
|
HeadquarterAddress: derefStringOrNotSpecified(v.HeadquarterAddress),
|
||||||
|
WebsiteURL: derefStringOrNotSpecified(v.WebsiteURL),
|
||||||
|
PrivacyPolicyURL: derefStringOrNotSpecified(v.PrivacyPolicyURL),
|
||||||
|
ServiceLevelAgreementURL: derefStringOrNotSpecified(v.ServiceLevelAgreementURL),
|
||||||
|
DataProcessingAgreementURL: derefStringOrNotSpecified(v.DataProcessingAgreementURL),
|
||||||
|
BusinessAssociateAgreementURL: derefStringOrNotSpecified(v.BusinessAssociateAgreementURL),
|
||||||
|
SubprocessorsListURL: derefStringOrNotSpecified(v.SubprocessorsListURL),
|
||||||
|
StatusPageURL: derefStringOrNotSpecified(v.StatusPageURL),
|
||||||
|
TermsOfServiceURL: derefStringOrNotSpecified(v.TermsOfServiceURL),
|
||||||
|
SecurityPageURL: derefStringOrNotSpecified(v.SecurityPageURL),
|
||||||
|
TrustPageURL: derefStringOrNotSpecified(v.TrustPageURL),
|
||||||
|
Certifications: joinOrNotSpecified(v.Certifications),
|
||||||
|
Countries: formatCountries(v.Countries),
|
||||||
|
BusinessOwner: lookupProfileName(profileMap, v.BusinessOwnerID),
|
||||||
|
SecurityOwner: lookupProfileName(profileMap, v.SecurityOwnerID),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, vs := range servicesByVendor[v.ID] {
|
||||||
|
row.Services = append(row.Services, docgen.VendorListService{
|
||||||
|
Name: vs.Name,
|
||||||
|
Description: derefStringOrNotSpecified(vs.Description),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range contactsByVendor[v.ID] {
|
||||||
|
email := ""
|
||||||
|
if c.Email != nil {
|
||||||
|
email = c.Email.String()
|
||||||
|
}
|
||||||
|
row.Contacts = append(row.Contacts, docgen.VendorListContact{
|
||||||
|
FullName: derefStringOrNotSpecified(c.FullName),
|
||||||
|
Email: stringOrNotSpecified(email),
|
||||||
|
Phone: derefStringOrNotSpecified(c.Phone),
|
||||||
|
Role: derefStringOrNotSpecified(c.Role),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ra := range assessmentsByVendor[v.ID] {
|
||||||
|
row.RiskAssessments = append(row.RiskAssessments, docgen.VendorListRiskAssessment{
|
||||||
|
AssessedAt: ra.CreatedAt.Format("2006-01-02"),
|
||||||
|
ExpiresAt: ra.ExpiresAt.Format("2006-01-02"),
|
||||||
|
DataSensitivity: formatDataSensitivity(ra.DataSensitivity),
|
||||||
|
BusinessImpact: formatBusinessImpact(ra.BusinessImpact),
|
||||||
|
Notes: derefStringOrNotSpecified(ra.Notes),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range reportsByVendor[v.ID] {
|
||||||
|
row.ComplianceReports = append(row.ComplianceReports, docgen.VendorListComplianceReport{
|
||||||
|
ReportName: r.ReportName,
|
||||||
|
ReportDate: r.ReportDate.Format("2006-01-02"),
|
||||||
|
ValidUntil: formatTimeOrNotSpecified(r.ValidUntil),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if baa := baaByVendor[v.ID]; baa != nil {
|
||||||
|
row.BusinessAssociateAgreement = &docgen.VendorListAgreement{
|
||||||
|
ValidFrom: formatTimeOrNotSpecified(baa.ValidFrom),
|
||||||
|
ValidUntil: formatTimeOrNotSpecified(baa.ValidUntil),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dpa := dpaByVendor[v.ID]; dpa != nil {
|
||||||
|
row.DataPrivacyAgreement = &docgen.VendorListAgreement{
|
||||||
|
ValidFrom: formatTimeOrNotSpecified(dpa.ValidFrom),
|
||||||
|
ValidUntil: formatTimeOrNotSpecified(dpa.ValidUntil),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = append(rows, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return docgen.VendorListData{
|
||||||
|
Title: "Vendors",
|
||||||
|
OrganizationName: organization.Name,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
TotalVendors: len(vendors),
|
||||||
|
Rows: rows,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringOrNotSpecified(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return "Not specified"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatTimeOrNotSpecified(t *time.Time) string {
|
||||||
|
if t == nil {
|
||||||
|
return "Not specified"
|
||||||
|
}
|
||||||
|
return t.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinOrNotSpecified(items []string) string {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return "Not specified"
|
||||||
|
}
|
||||||
|
return strings.Join(items, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatCountries(c coredata.CountryCodes) string {
|
||||||
|
if len(c) == 0 {
|
||||||
|
return "Not specified"
|
||||||
|
}
|
||||||
|
parts := make([]string, len(c))
|
||||||
|
for i, cc := range c {
|
||||||
|
parts[i] = string(cc)
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupProfileName(profiles map[gid.GID]*coredata.MembershipProfile, id *gid.GID) string {
|
||||||
|
if id == nil {
|
||||||
|
return "Not assigned"
|
||||||
|
}
|
||||||
|
if p, ok := profiles[*id]; ok {
|
||||||
|
return p.FullName
|
||||||
|
}
|
||||||
|
return "Not assigned"
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDataSensitivity(s coredata.DataSensitivity) string {
|
||||||
|
switch s {
|
||||||
|
case coredata.DataSensitivityNone:
|
||||||
|
return "None"
|
||||||
|
case coredata.DataSensitivityLow:
|
||||||
|
return "Low"
|
||||||
|
case coredata.DataSensitivityMedium:
|
||||||
|
return "Medium"
|
||||||
|
case coredata.DataSensitivityHigh:
|
||||||
|
return "High"
|
||||||
|
case coredata.DataSensitivityCritical:
|
||||||
|
return "Critical"
|
||||||
|
default:
|
||||||
|
return string(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatBusinessImpact(b coredata.BusinessImpact) string {
|
||||||
|
switch b {
|
||||||
|
case coredata.BusinessImpactLow:
|
||||||
|
return "Low"
|
||||||
|
case coredata.BusinessImpactMedium:
|
||||||
|
return "Medium"
|
||||||
|
case coredata.BusinessImpactHigh:
|
||||||
|
return "High"
|
||||||
|
case coredata.BusinessImpactCritical:
|
||||||
|
return "Critical"
|
||||||
|
default:
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatVendorCategory(c coredata.VendorCategory) string {
|
||||||
|
switch c {
|
||||||
|
case coredata.VendorCategoryAnalytics:
|
||||||
|
return "Analytics"
|
||||||
|
case coredata.VendorCategoryCloudMonitoring:
|
||||||
|
return "Cloud Monitoring"
|
||||||
|
case coredata.VendorCategoryCloudProvider:
|
||||||
|
return "Cloud Provider"
|
||||||
|
case coredata.VendorCategoryCollaboration:
|
||||||
|
return "Collaboration"
|
||||||
|
case coredata.VendorCategoryCustomerSupport:
|
||||||
|
return "Customer Support"
|
||||||
|
case coredata.VendorCategoryDataStorageAndProcessing:
|
||||||
|
return "Data Storage and Processing"
|
||||||
|
case coredata.VendorCategoryDocumentManagement:
|
||||||
|
return "Document Management"
|
||||||
|
case coredata.VendorCategoryEmployeeManagement:
|
||||||
|
return "Employee Management"
|
||||||
|
case coredata.VendorCategoryEngineering:
|
||||||
|
return "Engineering"
|
||||||
|
case coredata.VendorCategoryFinance:
|
||||||
|
return "Finance"
|
||||||
|
case coredata.VendorCategoryIdentityProvider:
|
||||||
|
return "Identity Provider"
|
||||||
|
case coredata.VendorCategoryIT:
|
||||||
|
return "IT"
|
||||||
|
case coredata.VendorCategoryMarketing:
|
||||||
|
return "Marketing"
|
||||||
|
case coredata.VendorCategoryOfficeOperations:
|
||||||
|
return "Office Operations"
|
||||||
|
case coredata.VendorCategoryOther:
|
||||||
|
return "Other"
|
||||||
|
case coredata.VendorCategoryPasswordManagement:
|
||||||
|
return "Password Management"
|
||||||
|
case coredata.VendorCategoryProductAndDesign:
|
||||||
|
return "Product and Design"
|
||||||
|
case coredata.VendorCategoryProfessionalServices:
|
||||||
|
return "Professional Services"
|
||||||
|
case coredata.VendorCategoryRecruiting:
|
||||||
|
return "Recruiting"
|
||||||
|
case coredata.VendorCategorySales:
|
||||||
|
return "Sales"
|
||||||
|
case coredata.VendorCategorySecurity:
|
||||||
|
return "Security"
|
||||||
|
case coredata.VendorCategoryVersionControl:
|
||||||
|
return "Version Control"
|
||||||
|
default:
|
||||||
|
return string(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var vendorListTemplate = template.Must(
|
||||||
|
template.New("vendor_list.json.tmpl").
|
||||||
|
Funcs(template.FuncMap{
|
||||||
|
"json": func(v any) (string, error) {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
},
|
||||||
|
"printf": fmt.Sprintf,
|
||||||
|
"add": func(a, b int) int { return a + b },
|
||||||
|
}).
|
||||||
|
ParseFS(Templates, "templates/vendor_list.json.tmpl"),
|
||||||
|
)
|
||||||
|
|
||||||
|
func BuildVendorListDocument(data docgen.VendorListData) (string, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := vendorListTemplate.Execute(&buf, data); err != nil {
|
||||||
|
return "", fmt.Errorf("cannot execute vendor list template: %w", err)
|
||||||
|
}
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|||||||
302
pkg/probo/templates/vendor_list.json.tmpl
Normal file
302
pkg/probo/templates/vendor_list.json.tmpl
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
{
|
||||||
|
"type": "doc",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 1 },
|
||||||
|
"content": [{ "type": "text", "text": "1. Purpose" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [{ "type": "text", "text": "This document provides a comprehensive register of all vendors used by the organization. It captures vendor profile information, services consumed, contacts, risk assessments, compliance reports, and contractual agreements (BAA, DPA) for each vendor." }]
|
||||||
|
},
|
||||||
|
{ "type": "horizontalRule" },
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 1 },
|
||||||
|
"content": [{ "type": "text", "text": "2. Vendors" }]
|
||||||
|
}{{range $i, $r := .Rows}},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 2 },
|
||||||
|
"content": [{ "type": "text", "text": {{json (printf "2.%d %s" (add $i 1) $r.Name)}} }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": {{json (printf "2.%d.1 General Information" (add $i 1))}} }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Legal Name: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.LegalName}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Description: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.Description}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Category: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.Category}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Headquarter Address: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.HeadquarterAddress}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Countries: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.Countries}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Certifications: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.Certifications}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": {{json (printf "2.%d.2 URLs & Pages" (add $i 1))}} }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Website: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.WebsiteURL}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Privacy Policy: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.PrivacyPolicyURL}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Service Level Agreement: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.ServiceLevelAgreementURL}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Data Processing Agreement: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.DataProcessingAgreementURL}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Business Associate Agreement: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.BusinessAssociateAgreementURL}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Subprocessors List: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.SubprocessorsListURL}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Status Page: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.StatusPageURL}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Terms of Service: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.TermsOfServiceURL}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Security Page: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.SecurityPageURL}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Trust Page: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.TrustPageURL}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": {{json (printf "2.%d.3 Owners" (add $i 1))}} }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Business Owner: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.BusinessOwner}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Security Owner: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.SecurityOwner}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": {{json (printf "2.%d.4 Services" (add $i 1))}} }]
|
||||||
|
}{{if $r.Services}}{{range $r.Services}},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": {{json (printf "%s — " .Name)}}, "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json .Description}} }
|
||||||
|
]
|
||||||
|
}{{end}}{{else}},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [{ "type": "text", "text": "No services recorded." }]
|
||||||
|
}{{end}},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": {{json (printf "2.%d.5 Contacts" (add $i 1))}} }]
|
||||||
|
}{{if $r.Contacts}}{{range $r.Contacts}},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": {{json (printf "%s — " .FullName)}}, "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json (printf "%s, %s, %s" .Role .Email .Phone)}} }
|
||||||
|
]
|
||||||
|
}{{end}}{{else}},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [{ "type": "text", "text": "No contacts recorded." }]
|
||||||
|
}{{end}},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": {{json (printf "2.%d.6 Risk Assessments" (add $i 1))}} }]
|
||||||
|
}{{if $r.RiskAssessments}}{{range $r.RiskAssessments}},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Assessed on: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json .AssessedAt}} },
|
||||||
|
{ "type": "text", "text": " · Expires on: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json .ExpiresAt}} },
|
||||||
|
{ "type": "text", "text": " · Data Sensitivity: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json .DataSensitivity}} },
|
||||||
|
{ "type": "text", "text": " · Business Impact: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json .BusinessImpact}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Notes: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json .Notes}} }
|
||||||
|
]
|
||||||
|
}{{end}}{{else}},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [{ "type": "text", "text": "No risk assessments recorded." }]
|
||||||
|
}{{end}},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": {{json (printf "2.%d.7 Compliance Reports" (add $i 1))}} }]
|
||||||
|
}{{if $r.ComplianceReports}}{{range $r.ComplianceReports}},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": {{json (printf "%s — " .ReportName)}}, "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json (printf "Date: %s · Valid Until: %s" .ReportDate .ValidUntil)}} }
|
||||||
|
]
|
||||||
|
}{{end}}{{else}},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [{ "type": "text", "text": "No compliance reports recorded." }]
|
||||||
|
}{{end}},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": {{json (printf "2.%d.8 Agreements" (add $i 1))}} }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Business Associate Agreement: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{if $r.BusinessAssociateAgreement}}{{json (printf "Yes — From %s until %s" $r.BusinessAssociateAgreement.ValidFrom $r.BusinessAssociateAgreement.ValidUntil)}}{{else}}"No"{{end}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Data Privacy Agreement: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{if $r.DataPrivacyAgreement}}{{json (printf "Yes — From %s until %s" $r.DataPrivacyAgreement.ValidFrom $r.DataPrivacyAgreement.ValidUntil)}}{{else}}"No"{{end}} }
|
||||||
|
]
|
||||||
|
}{{end}},
|
||||||
|
{ "type": "horizontalRule" },
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 1 },
|
||||||
|
"content": [{ "type": "text", "text": "3. Annexes" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 2 },
|
||||||
|
"content": [{ "type": "text", "text": "3.1 Lexicon" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": "Data Sensitivity" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bulletList",
|
||||||
|
"content": [
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "None: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "No sensitive data is shared with the vendor." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Low: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "The vendor processes low sensitivity data such as public information." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Medium: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "The vendor processes medium sensitivity data such as internal business data." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "High: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "The vendor processes high sensitivity data such as personal data, financial data, or trade secrets." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Critical: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "The vendor processes the most sensitive categories of data, where unauthorized disclosure would cause severe harm." }] }] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": "Business Impact" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bulletList",
|
||||||
|
"content": [
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Low: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Limited disruption to operations if the vendor service is unavailable." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Medium: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Significant disruption to operations if the vendor service is unavailable." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "High: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Severe disruption or outage if the vendor service is unavailable." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Critical: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Operations cannot continue if the vendor service is unavailable; immediate business-wide impact." }] }] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -329,9 +329,10 @@ type Organization implements Node {
|
|||||||
last: Int
|
last: Int
|
||||||
before: CursorKey
|
before: CursorKey
|
||||||
orderBy: VendorOrder
|
orderBy: VendorOrder
|
||||||
filter: VendorFilter = { snapshotId: null }
|
|
||||||
): VendorConnection! @goField(forceResolver: true)
|
): VendorConnection! @goField(forceResolver: true)
|
||||||
|
|
||||||
|
vendorsDocument: Document @goField(forceResolver: true)
|
||||||
|
|
||||||
webhookSubscriptions(
|
webhookSubscriptions(
|
||||||
first: Int
|
first: Int
|
||||||
after: CursorKey
|
after: CursorKey
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
enum SnapshotsType
|
enum SnapshotsType
|
||||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.SnapshotsType") {
|
@goModel(model: "go.probo.inc/probo/pkg/coredata.SnapshotsType") {
|
||||||
RISKS @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeRisks")
|
RISKS @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeRisks")
|
||||||
VENDORS
|
|
||||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeVendors")
|
|
||||||
PROCESSING_ACTIVITIES
|
|
||||||
@goEnum(
|
|
||||||
value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeProcessingActivities"
|
|
||||||
)
|
|
||||||
STATEMENTS_OF_APPLICABILITY
|
|
||||||
@goEnum(
|
|
||||||
value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeStatementsOfApplicability"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SnapshotOrderField
|
enum SnapshotOrderField
|
||||||
|
|||||||
@@ -206,13 +206,8 @@ input VendorRiskAssessmentOrder {
|
|||||||
direction: OrderDirection!
|
direction: OrderDirection!
|
||||||
}
|
}
|
||||||
|
|
||||||
input VendorFilter {
|
|
||||||
snapshotId: ID
|
|
||||||
}
|
|
||||||
|
|
||||||
type Vendor implements Node {
|
type Vendor implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
snapshotId: ID
|
|
||||||
name: String!
|
name: String!
|
||||||
category: VendorCategory!
|
category: VendorCategory!
|
||||||
description: String
|
description: String
|
||||||
@@ -462,6 +457,19 @@ extend type Mutation {
|
|||||||
input: CreateVendorRiskAssessmentInput!
|
input: CreateVendorRiskAssessmentInput!
|
||||||
): CreateVendorRiskAssessmentPayload!
|
): CreateVendorRiskAssessmentPayload!
|
||||||
assessVendor(input: AssessVendorInput!): AssessVendorPayload!
|
assessVendor(input: AssessVendorInput!): AssessVendorPayload!
|
||||||
|
publishVendorList(
|
||||||
|
input: PublishVendorListInput!
|
||||||
|
): PublishVendorListPayload!
|
||||||
|
}
|
||||||
|
|
||||||
|
input PublishVendorListInput {
|
||||||
|
organizationId: ID!
|
||||||
|
approverIds: [ID!]
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublishVendorListPayload {
|
||||||
|
documentEdge: DocumentEdge!
|
||||||
|
documentVersionEdge: DocumentVersionEdge!
|
||||||
}
|
}
|
||||||
|
|
||||||
input CreateVendorInput {
|
input CreateVendorInput {
|
||||||
|
|||||||
@@ -1210,7 +1210,7 @@ func (r *organizationResolver) CookieBanners(ctx context.Context, obj *types.Org
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Vendors is the resolver for the vendors field.
|
// Vendors is the resolver for the vendors field.
|
||||||
func (r *organizationResolver) Vendors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy, filter *types.VendorFilter) (*types.VendorConnection, error) {
|
func (r *organizationResolver) Vendors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy) (*types.VendorConnection, error) {
|
||||||
if err := r.authorize(ctx, obj.ID, probo.ActionVendorList); err != nil {
|
if err := r.authorize(ctx, obj.ID, probo.ActionVendorList); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1230,10 +1230,7 @@ func (r *organizationResolver) Vendors(ctx context.Context, obj *types.Organizat
|
|||||||
|
|
||||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||||
|
|
||||||
var vendorFilter = coredata.NewVendorFilter(nil, nil)
|
vendorFilter := coredata.NewVendorFilter(nil)
|
||||||
if filter != nil {
|
|
||||||
vendorFilter = coredata.NewVendorFilter(&filter.SnapshotID, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
page, err := prb.Vendors.ListForOrganizationID(ctx, obj.ID, cursor, vendorFilter)
|
page, err := prb.Vendors.ListForOrganizationID(ctx, obj.ID, cursor, vendorFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1244,6 +1241,35 @@ func (r *organizationResolver) Vendors(ctx context.Context, obj *types.Organizat
|
|||||||
return types.NewVendorConnection(page, r, obj.ID), nil
|
return types.NewVendorConnection(page, r, obj.ID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VendorsDocument is the resolver for the vendorsDocument field.
|
||||||
|
func (r *organizationResolver) VendorsDocument(ctx context.Context, obj *types.Organization) (*types.Document, error) {
|
||||||
|
if err := r.authorize(ctx, obj.ID, probo.ActionDocumentGet); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prb := r.ProboService(ctx, obj.ID.TenantID())
|
||||||
|
|
||||||
|
documentID, err := prb.GeneratedDocuments.GetVendorsDocumentID(ctx, obj.ID)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot get vendors document ID", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
if documentID == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
document, err := prb.Documents.Get(ctx, *documentID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot load vendors document", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.NewDocument(document), nil
|
||||||
|
}
|
||||||
|
|
||||||
// WebhookSubscriptions is the resolver for the webhookSubscriptions field.
|
// WebhookSubscriptions is the resolver for the webhookSubscriptions field.
|
||||||
func (r *organizationResolver) WebhookSubscriptions(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.WebhookSubscriptionOrderBy) (*types.WebhookSubscriptionConnection, error) {
|
func (r *organizationResolver) WebhookSubscriptions(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.WebhookSubscriptionOrderBy) (*types.WebhookSubscriptionConnection, error) {
|
||||||
if err := r.authorize(ctx, obj.ID, probo.ActionWebhookSubscriptionList); err != nil {
|
if err := r.authorize(ctx, obj.ID, probo.ActionWebhookSubscriptionList); err != nil {
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ func NewVendor(v *coredata.Vendor) *Vendor {
|
|||||||
WebsiteURL: v.WebsiteURL,
|
WebsiteURL: v.WebsiteURL,
|
||||||
Category: v.Category,
|
Category: v.Category,
|
||||||
ShowOnTrustCenter: v.ShowOnTrustCenter,
|
ShowOnTrustCenter: v.ShowOnTrustCenter,
|
||||||
SnapshotID: v.SnapshotID,
|
|
||||||
Countries: v.Countries,
|
Countries: v.Countries,
|
||||||
UpdatedAt: v.UpdatedAt,
|
UpdatedAt: v.UpdatedAt,
|
||||||
CreatedAt: v.CreatedAt,
|
CreatedAt: v.CreatedAt,
|
||||||
|
|||||||
@@ -565,6 +565,29 @@ func (r *mutationResolver) AssessVendor(ctx context.Context, input types.AssessV
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PublishVendorList is the resolver for the publishVendorList field.
|
||||||
|
func (r *mutationResolver) PublishVendorList(ctx context.Context, input types.PublishVendorListInput) (*types.PublishVendorListPayload, error) {
|
||||||
|
if err := r.authorize(ctx, input.OrganizationID, probo.ActionVendorPublish); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prb := r.ProboService(ctx, input.OrganizationID.TenantID())
|
||||||
|
|
||||||
|
document, documentVersion, err := prb.GeneratedDocuments.PublishVendorList(ctx, input.OrganizationID, input.ApproverIds)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
||||||
|
return nil, gqlutils.Conflict(ctx, err)
|
||||||
|
}
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot publish vendor list", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.PublishVendorListPayload{
|
||||||
|
DocumentEdge: types.NewDocumentEdge(document, coredata.DocumentOrderFieldCreatedAt),
|
||||||
|
DocumentVersionEdge: types.NewDocumentVersionEdge(documentVersion, coredata.DocumentVersionOrderFieldCreatedAt),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Organization is the resolver for the organization field.
|
// Organization is the resolver for the organization field.
|
||||||
func (r *vendorResolver) Organization(ctx context.Context, obj *types.Vendor) (*types.Organization, error) {
|
func (r *vendorResolver) Organization(ctx context.Context, obj *types.Vendor) (*types.Organization, error) {
|
||||||
if err := r.authorize(ctx, obj.ID, probo.ActionOrganizationGet); err != nil {
|
if err := r.authorize(ctx, obj.ID, probo.ActionOrganizationGet); err != nil {
|
||||||
|
|||||||
@@ -63,11 +63,7 @@ func (r *Resolver) ListVendorsTool(ctx context.Context, req *mcp.CallToolRequest
|
|||||||
|
|
||||||
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
|
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
|
||||||
|
|
||||||
noSnapshot := (*gid.GID)(nil)
|
vendorFilter := coredata.NewVendorFilter(nil)
|
||||||
vendorFilter := coredata.NewVendorFilter(&noSnapshot, nil)
|
|
||||||
if input.Filter != nil {
|
|
||||||
vendorFilter = coredata.NewVendorFilter(&input.Filter.SnapshotID, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
page, err := prb.Vendors.ListForOrganizationID(ctx, input.OrganizationID, cursor, vendorFilter)
|
page, err := prb.Vendors.ListForOrganizationID(ctx, input.OrganizationID, cursor, vendorFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -4840,3 +4836,19 @@ func (r *Resolver) PublishTransferImpactAssessmentListTool(ctx context.Context,
|
|||||||
DocumentVersionID: documentVersion.ID,
|
DocumentVersionID: documentVersion.ID,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) PublishVendorListTool(ctx context.Context, req *mcp.CallToolRequest, input *types.PublishVendorListInput) (*mcp.CallToolResult, types.PublishVendorListOutput, error) {
|
||||||
|
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionVendorPublish)
|
||||||
|
|
||||||
|
svc := r.ProboService(ctx, input.OrganizationID)
|
||||||
|
|
||||||
|
document, documentVersion, err := svc.GeneratedDocuments.PublishVendorList(ctx, input.OrganizationID, input.ApproverIds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.PublishVendorListOutput{}, fmt.Errorf("cannot publish vendor list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, types.PublishVendorListOutput{
|
||||||
|
DocumentID: document.ID,
|
||||||
|
DocumentVersionID: documentVersion.ID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -362,15 +362,6 @@ components:
|
|||||||
cursor:
|
cursor:
|
||||||
$ref: "#/components/schemas/CursorKey"
|
$ref: "#/components/schemas/CursorKey"
|
||||||
description: Page cursor
|
description: Page cursor
|
||||||
filter:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
snapshot_id:
|
|
||||||
anyOf:
|
|
||||||
- $ref: "#/components/schemas/GID"
|
|
||||||
- type: "null"
|
|
||||||
description: Filter by snapshot ID. Defaults to null, which returns only vendors with no snapshot (current live data). Pass a specific snapshot ID to retrieve vendors as they were at that snapshot.
|
|
||||||
default: null
|
|
||||||
|
|
||||||
ListVendorsOutput:
|
ListVendorsOutput:
|
||||||
type: object
|
type: object
|
||||||
@@ -560,11 +551,6 @@ components:
|
|||||||
- string
|
- string
|
||||||
- "null"
|
- "null"
|
||||||
description: Notes
|
description: Notes
|
||||||
snapshot_id:
|
|
||||||
anyOf:
|
|
||||||
- $ref: "#/components/schemas/GID"
|
|
||||||
- type: "null"
|
|
||||||
description: Snapshot ID
|
|
||||||
created_at:
|
created_at:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
@@ -5372,7 +5358,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
- RISKS
|
- RISKS
|
||||||
- VENDORS
|
|
||||||
- NONCONFORMITIES
|
- NONCONFORMITIES
|
||||||
- OBLIGATIONS
|
- OBLIGATIONS
|
||||||
- CONTINUAL_IMPROVEMENTS
|
- CONTINUAL_IMPROVEMENTS
|
||||||
@@ -7119,6 +7104,33 @@ components:
|
|||||||
$ref: "#/components/schemas/GID"
|
$ref: "#/components/schemas/GID"
|
||||||
description: Created document version ID
|
description: Created document version ID
|
||||||
|
|
||||||
|
PublishVendorListInput:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- organization_id
|
||||||
|
properties:
|
||||||
|
organization_id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: Organization ID
|
||||||
|
approver_ids:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: Optional approver profile IDs. If provided, creates a draft pending approval instead of publishing immediately.
|
||||||
|
|
||||||
|
PublishVendorListOutput:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- document_id
|
||||||
|
- document_version_id
|
||||||
|
properties:
|
||||||
|
document_id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: Created or updated document ID
|
||||||
|
document_version_id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: Created document version ID
|
||||||
|
|
||||||
PublishStatementOfApplicabilityInput:
|
PublishStatementOfApplicabilityInput:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -10396,6 +10408,14 @@ tools:
|
|||||||
$ref: "#/components/schemas/PublishTransferImpactAssessmentListInput"
|
$ref: "#/components/schemas/PublishTransferImpactAssessmentListInput"
|
||||||
outputSchema:
|
outputSchema:
|
||||||
$ref: "#/components/schemas/PublishTransferImpactAssessmentListOutput"
|
$ref: "#/components/schemas/PublishTransferImpactAssessmentListOutput"
|
||||||
|
- name: publishVendorList
|
||||||
|
description: Publish the vendor register for an organization as a document. If a document already exists, a new version is created.
|
||||||
|
hints:
|
||||||
|
readonly: false
|
||||||
|
inputSchema:
|
||||||
|
$ref: "#/components/schemas/PublishVendorListInput"
|
||||||
|
outputSchema:
|
||||||
|
$ref: "#/components/schemas/PublishVendorListOutput"
|
||||||
- name: publishStatementOfApplicability
|
- name: publishStatementOfApplicability
|
||||||
description: Publish a statement of applicability as a document. If a document already exists, a new version is created.
|
description: Publish a statement of applicability as a document. If a document already exists, a new version is created.
|
||||||
hints:
|
hints:
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ func NewVendorRiskAssessment(v *coredata.VendorRiskAssessment) *VendorRiskAssess
|
|||||||
DataSensitivity: v.DataSensitivity,
|
DataSensitivity: v.DataSensitivity,
|
||||||
BusinessImpact: v.BusinessImpact,
|
BusinessImpact: v.BusinessImpact,
|
||||||
Notes: v.Notes,
|
Notes: v.Notes,
|
||||||
SnapshotID: v.SnapshotID,
|
|
||||||
CreatedAt: v.CreatedAt,
|
CreatedAt: v.CreatedAt,
|
||||||
UpdatedAt: v.UpdatedAt,
|
UpdatedAt: v.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,8 +64,7 @@ func (s VendorService) ListForOrganizationId(
|
|||||||
ctx,
|
ctx,
|
||||||
func(ctx context.Context, conn pg.Querier) error {
|
func(ctx context.Context, conn pg.Querier) error {
|
||||||
showOnTrustCenter := true
|
showOnTrustCenter := true
|
||||||
var nilSnapshotID *gid.GID = nil
|
filter := coredata.NewVendorFilter(&showOnTrustCenter)
|
||||||
filter := coredata.NewVendorFilter(&nilSnapshotID, &showOnTrustCenter)
|
|
||||||
|
|
||||||
err := vendors.LoadByOrganizationID(ctx, conn, s.svc.scope, organizationID, cursor, filter)
|
err := vendors.LoadByOrganizationID(ctx, conn, s.svc.scope, organizationID, cursor, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -99,8 +98,7 @@ func (s VendorService) CountForTrustCenterId(
|
|||||||
|
|
||||||
vendors := &coredata.Vendors{}
|
vendors := &coredata.Vendors{}
|
||||||
showOnTrustCenter := true
|
showOnTrustCenter := true
|
||||||
var nilSnapshotID *gid.GID = nil
|
filter := coredata.NewVendorFilter(&showOnTrustCenter)
|
||||||
filter := coredata.NewVendorFilter(&nilSnapshotID, &showOnTrustCenter)
|
|
||||||
count, err = vendors.CountByOrganizationID(ctx, conn, s.svc.scope, trustCenter.OrganizationID, filter)
|
count, err = vendors.CountByOrganizationID(ctx, conn, s.svc.scope, trustCenter.OrganizationID, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot count vendors: %w", err)
|
return fmt.Errorf("cannot count vendors: %w", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user