From 8cd014ebbc76087b51eba712470abce9c07b85da Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Thu, 30 Oct 2025 15:09:57 +0100 Subject: [PATCH] Add vendors to processing activities Signed-off-by: Sacha Al Himdani --- .../hooks/graph/ProcessingActivityGraph.ts | 31 +++ ...singActivityGraphCreateMutation.graphql.ts | 100 ++++++-- ...rocessingActivityGraphNodeQuery.graphql.ts | 84 +++++- ...singActivityGraphUpdateMutation.graphql.ts | 102 ++++++-- .../ProcessingActivityDetailsPage.tsx | 16 ++ .../CreateProcessingActivityDialog.tsx | 12 + pkg/coredata/migrations/20251030T120000Z.sql | 8 + pkg/coredata/processing_activities.go | 18 ++ pkg/coredata/processing_activity_vendor.go | 177 +++++++++++++ pkg/coredata/vendor.go | 200 +++++++++++++++ pkg/probo/processing_activity_service.go | 16 ++ pkg/probo/vendor_service.go | 26 ++ pkg/server/api/console/v1/schema.graphql | 9 + pkg/server/api/console/v1/schema/schema.go | 240 +++++++++++++++++- pkg/server/api/console/v1/types/types.go | 3 + pkg/server/api/console/v1/v1_resolver.go | 27 ++ 16 files changed, 1018 insertions(+), 51 deletions(-) create mode 100644 pkg/coredata/migrations/20251030T120000Z.sql create mode 100644 pkg/coredata/processing_activity_vendor.go diff --git a/apps/console/src/hooks/graph/ProcessingActivityGraph.ts b/apps/console/src/hooks/graph/ProcessingActivityGraph.ts index c1f7e6f8b..34ecb6de8 100644 --- a/apps/console/src/hooks/graph/ProcessingActivityGraph.ts +++ b/apps/console/src/hooks/graph/ProcessingActivityGraph.ts @@ -38,6 +38,16 @@ export const processingActivityNodeQuery = graphql` securityMeasures dataProtectionImpactAssessment transferImpactAssessment + vendors(first: 50) { + edges { + node { + id + name + websiteUrl + category + } + } + } organization { id name @@ -73,6 +83,15 @@ export const createProcessingActivityMutation = graphql` securityMeasures dataProtectionImpactAssessment transferImpactAssessment + vendors(first: 50) { + edges { + node { + id + name + websiteUrl + } + } + } createdAt } } @@ -100,6 +119,15 @@ export const updateProcessingActivityMutation = graphql` securityMeasures dataProtectionImpactAssessment transferImpactAssessment + vendors(first: 50) { + edges { + node { + id + name + websiteUrl + } + } + } updatedAt } } @@ -172,6 +200,7 @@ export const useCreateProcessingActivity = (connectionId?: string) => { securityMeasures?: string; dataProtectionImpactAssessment?: string; transferImpactAssessment?: string; + vendorIds?: string[]; }) => { if (!input.organizationId) { return alert(__("Failed to create processing activity: organization is required")); @@ -199,6 +228,7 @@ export const useCreateProcessingActivity = (connectionId?: string) => { securityMeasures: input.securityMeasures, dataProtectionImpactAssessment: input.dataProtectionImpactAssessment, transferImpactAssessment: input.transferImpactAssessment, + vendorIds: input.vendorIds, }, connections: connectionId ? [connectionId] : [], }, @@ -227,6 +257,7 @@ export const useUpdateProcessingActivity = () => { securityMeasures?: string; dataProtectionImpactAssessment?: string; transferImpactAssessment?: string; + vendorIds?: string[]; }) => { if (!input.id) { return alert(__("Failed to update processing activity: ID is required")); diff --git a/apps/console/src/hooks/graph/__generated__/ProcessingActivityGraphCreateMutation.graphql.ts b/apps/console/src/hooks/graph/__generated__/ProcessingActivityGraphCreateMutation.graphql.ts index 6c9fe791c..16ade470f 100644 --- a/apps/console/src/hooks/graph/__generated__/ProcessingActivityGraphCreateMutation.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/ProcessingActivityGraphCreateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -31,6 +31,7 @@ export type CreateProcessingActivityInput = { specialOrCriminalData: ProcessingActivitySpecialOrCriminalData; transferImpactAssessment: ProcessingActivityTransferImpactAssessment; transferSafeguards?: ProcessingActivityTransferSafeguards | null | undefined; + vendorIds?: ReadonlyArray | null | undefined; }; export type ProcessingActivityGraphCreateMutation$variables = { connections: ReadonlyArray; @@ -57,6 +58,15 @@ export type ProcessingActivityGraphCreateMutation$data = { readonly specialOrCriminalData: ProcessingActivitySpecialOrCriminalData; readonly transferImpactAssessment: ProcessingActivityTransferImpactAssessment; readonly transferSafeguards: ProcessingActivityTransferSafeguards | null | undefined; + readonly vendors: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly name: string; + readonly websiteUrl: string | null | undefined; + }; + }>; + }; }; }; }; @@ -85,6 +95,20 @@ v2 = [ } ], v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}, +v5 = { "alias": null, "args": null, "concreteType": "ProcessingActivityEdge", @@ -100,20 +124,8 @@ v3 = { "name": "node", "plural": false, "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null - }, + (v3/*: any*/), + (v4/*: any*/), { "alias": null, "args": null, @@ -212,6 +224,54 @@ v3 = { "name": "transferImpactAssessment", "storageKey": null }, + { + "alias": null, + "args": [ + { + "kind": "Literal", + "name": "first", + "value": 50 + } + ], + "concreteType": "VendorConnection", + "kind": "LinkedField", + "name": "vendors", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Vendor", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + (v4/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "websiteUrl", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "vendors(first:50)" + }, { "alias": null, "args": null, @@ -243,7 +303,7 @@ return { "name": "createProcessingActivity", "plural": false, "selections": [ - (v3/*: any*/) + (v5/*: any*/) ], "storageKey": null } @@ -268,7 +328,7 @@ return { "name": "createProcessingActivity", "plural": false, "selections": [ - (v3/*: any*/), + (v5/*: any*/), { "alias": null, "args": null, @@ -291,16 +351,16 @@ return { ] }, "params": { - "cacheID": "3a675caa3773822d9b478ad1669eb69f", + "cacheID": "4992c543938b28bc08d2316602d7bd1e", "id": null, "metadata": {}, "name": "ProcessingActivityGraphCreateMutation", "operationKind": "mutation", - "text": "mutation ProcessingActivityGraphCreateMutation(\n $input: CreateProcessingActivityInput!\n) {\n createProcessingActivity(input: $input) {\n processingActivityEdge {\n node {\n id\n name\n purpose\n dataSubjectCategory\n personalDataCategory\n specialOrCriminalData\n consentEvidenceLink\n lawfulBasis\n recipients\n location\n internationalTransfers\n transferSafeguards\n retentionPeriod\n securityMeasures\n dataProtectionImpactAssessment\n transferImpactAssessment\n createdAt\n }\n }\n }\n}\n" + "text": "mutation ProcessingActivityGraphCreateMutation(\n $input: CreateProcessingActivityInput!\n) {\n createProcessingActivity(input: $input) {\n processingActivityEdge {\n node {\n id\n name\n purpose\n dataSubjectCategory\n personalDataCategory\n specialOrCriminalData\n consentEvidenceLink\n lawfulBasis\n recipients\n location\n internationalTransfers\n transferSafeguards\n retentionPeriod\n securityMeasures\n dataProtectionImpactAssessment\n transferImpactAssessment\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n createdAt\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "bd2e03818df99d5b692216d738b18583"; +(node as any).hash = "2150e01b5dfb4ebf1553817b7cb39f08"; export default node; diff --git a/apps/console/src/hooks/graph/__generated__/ProcessingActivityGraphNodeQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/ProcessingActivityGraphNodeQuery.graphql.ts index ed2be26f5..6be92f9ce 100644 --- a/apps/console/src/hooks/graph/__generated__/ProcessingActivityGraphNodeQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/ProcessingActivityGraphNodeQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<5dea7ccaf98a1223d9e4cc1ecdfbb0bc>> * @lightSyntaxTransform * @nogrep */ @@ -14,6 +14,7 @@ export type ProcessingActivityLawfulBasis = "CONSENT" | "CONTRACTUAL_NECESSITY" export type ProcessingActivitySpecialOrCriminalData = "NO" | "POSSIBLE" | "YES"; export type ProcessingActivityTransferImpactAssessment = "NEEDED" | "NOT_NEEDED"; export type ProcessingActivityTransferSafeguards = "ADEQUACY_DECISION" | "BINDING_CORPORATE_RULES" | "CERTIFICATION_MECHANISMS" | "CODES_OF_CONDUCT" | "DEROGATIONS" | "STANDARD_CONTRACTUAL_CLAUSES"; +export type VendorCategory = "ANALYTICS" | "CLOUD_MONITORING" | "CLOUD_PROVIDER" | "COLLABORATION" | "CUSTOMER_SUPPORT" | "DATA_STORAGE_AND_PROCESSING" | "DOCUMENT_MANAGEMENT" | "EMPLOYEE_MANAGEMENT" | "ENGINEERING" | "FINANCE" | "IDENTITY_PROVIDER" | "IT" | "MARKETING" | "OFFICE_OPERATIONS" | "OTHER" | "PASSWORD_MANAGEMENT" | "PRODUCT_AND_DESIGN" | "PROFESSIONAL_SERVICES" | "RECRUITING" | "SALES" | "SECURITY" | "VERSION_CONTROL"; export type ProcessingActivityGraphNodeQuery$variables = { processingActivityId: string; }; @@ -42,6 +43,16 @@ export type ProcessingActivityGraphNodeQuery$data = { readonly transferImpactAssessment?: ProcessingActivityTransferImpactAssessment; readonly transferSafeguards?: ProcessingActivityTransferSafeguards | null | undefined; readonly updatedAt?: any; + readonly vendors?: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly category: VendorCategory; + readonly id: string; + readonly name: string; + readonly websiteUrl: string | null | undefined; + }; + }>; + }; }; }; export type ProcessingActivityGraphNodeQuery = { @@ -184,6 +195,61 @@ v18 = { "storageKey": null }, v19 = { + "alias": null, + "args": [ + { + "kind": "Literal", + "name": "first", + "value": 50 + } + ], + "concreteType": "VendorConnection", + "kind": "LinkedField", + "name": "vendors", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Vendor", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v4/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "websiteUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "category", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "vendors(first:50)" +}, +v20 = { "alias": null, "args": null, "concreteType": "Organization", @@ -196,14 +262,14 @@ v19 = { ], "storageKey": null }, -v20 = { +v21 = { "alias": null, "args": null, "kind": "ScalarField", "name": "createdAt", "storageKey": null }, -v21 = { +v22 = { "alias": null, "args": null, "kind": "ScalarField", @@ -247,7 +313,8 @@ return { (v18/*: any*/), (v19/*: any*/), (v20/*: any*/), - (v21/*: any*/) + (v21/*: any*/), + (v22/*: any*/) ], "type": "ProcessingActivity", "abstractKey": null @@ -302,7 +369,8 @@ return { (v18/*: any*/), (v19/*: any*/), (v20/*: any*/), - (v21/*: any*/) + (v21/*: any*/), + (v22/*: any*/) ], "type": "ProcessingActivity", "abstractKey": null @@ -313,16 +381,16 @@ return { ] }, "params": { - "cacheID": "b2dd76939002ce8c523c7ac5a49c29f5", + "cacheID": "9e9d13d95bf99c04488b8bdd313e8f33", "id": null, "metadata": {}, "name": "ProcessingActivityGraphNodeQuery", "operationKind": "query", - "text": "query ProcessingActivityGraphNodeQuery(\n $processingActivityId: ID!\n) {\n node(id: $processingActivityId) {\n __typename\n ... on ProcessingActivity {\n id\n snapshotId\n name\n purpose\n dataSubjectCategory\n personalDataCategory\n specialOrCriminalData\n consentEvidenceLink\n lawfulBasis\n recipients\n location\n internationalTransfers\n transferSafeguards\n retentionPeriod\n securityMeasures\n dataProtectionImpactAssessment\n transferImpactAssessment\n organization {\n id\n name\n }\n createdAt\n updatedAt\n }\n id\n }\n}\n" + "text": "query ProcessingActivityGraphNodeQuery(\n $processingActivityId: ID!\n) {\n node(id: $processingActivityId) {\n __typename\n ... on ProcessingActivity {\n id\n snapshotId\n name\n purpose\n dataSubjectCategory\n personalDataCategory\n specialOrCriminalData\n consentEvidenceLink\n lawfulBasis\n recipients\n location\n internationalTransfers\n transferSafeguards\n retentionPeriod\n securityMeasures\n dataProtectionImpactAssessment\n transferImpactAssessment\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n category\n }\n }\n }\n organization {\n id\n name\n }\n createdAt\n updatedAt\n }\n id\n }\n}\n" } }; })(); -(node as any).hash = "7beac09f84d60e20e73a5b8e958908dd"; +(node as any).hash = "0112adb4f323533e0ce7a0922c5866d2"; export default node; diff --git a/apps/console/src/hooks/graph/__generated__/ProcessingActivityGraphUpdateMutation.graphql.ts b/apps/console/src/hooks/graph/__generated__/ProcessingActivityGraphUpdateMutation.graphql.ts index 759c5baef..0996cfe79 100644 --- a/apps/console/src/hooks/graph/__generated__/ProcessingActivityGraphUpdateMutation.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/ProcessingActivityGraphUpdateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<48427eb821d3a0f4e53430bb384d0305>> + * @generated SignedSource<<08099a83039838c6c79dbf1ca7c99034>> * @lightSyntaxTransform * @nogrep */ @@ -31,6 +31,7 @@ export type UpdateProcessingActivityInput = { specialOrCriminalData?: ProcessingActivitySpecialOrCriminalData | null | undefined; transferImpactAssessment?: ProcessingActivityTransferImpactAssessment | null | undefined; transferSafeguards?: ProcessingActivityTransferSafeguards | null | undefined; + vendorIds?: ReadonlyArray | null | undefined; }; export type ProcessingActivityGraphUpdateMutation$variables = { input: UpdateProcessingActivityInput; @@ -55,6 +56,15 @@ export type ProcessingActivityGraphUpdateMutation$data = { readonly transferImpactAssessment: ProcessingActivityTransferImpactAssessment; readonly transferSafeguards: ProcessingActivityTransferSafeguards | null | undefined; readonly updatedAt: any; + readonly vendors: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly name: string; + readonly websiteUrl: string | null | undefined; + }; + }>; + }; }; }; }; @@ -71,7 +81,21 @@ var v0 = [ "name": "input" } ], -v1 = [ +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}, +v3 = [ { "alias": null, "args": [ @@ -94,20 +118,8 @@ v1 = [ "name": "processingActivity", "plural": false, "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null - }, + (v1/*: any*/), + (v2/*: any*/), { "alias": null, "args": null, @@ -206,6 +218,54 @@ v1 = [ "name": "transferImpactAssessment", "storageKey": null }, + { + "alias": null, + "args": [ + { + "kind": "Literal", + "name": "first", + "value": 50 + } + ], + "concreteType": "VendorConnection", + "kind": "LinkedField", + "name": "vendors", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Vendor", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "websiteUrl", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "vendors(first:50)" + }, { "alias": null, "args": null, @@ -226,7 +286,7 @@ return { "kind": "Fragment", "metadata": null, "name": "ProcessingActivityGraphUpdateMutation", - "selections": (v1/*: any*/), + "selections": (v3/*: any*/), "type": "Mutation", "abstractKey": null }, @@ -235,19 +295,19 @@ return { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", "name": "ProcessingActivityGraphUpdateMutation", - "selections": (v1/*: any*/) + "selections": (v3/*: any*/) }, "params": { - "cacheID": "9ede6451e111196c25a7a4cee9ad86bf", + "cacheID": "8050213c76270c28b5543a629a956d47", "id": null, "metadata": {}, "name": "ProcessingActivityGraphUpdateMutation", "operationKind": "mutation", - "text": "mutation ProcessingActivityGraphUpdateMutation(\n $input: UpdateProcessingActivityInput!\n) {\n updateProcessingActivity(input: $input) {\n processingActivity {\n id\n name\n purpose\n dataSubjectCategory\n personalDataCategory\n specialOrCriminalData\n consentEvidenceLink\n lawfulBasis\n recipients\n location\n internationalTransfers\n transferSafeguards\n retentionPeriod\n securityMeasures\n dataProtectionImpactAssessment\n transferImpactAssessment\n updatedAt\n }\n }\n}\n" + "text": "mutation ProcessingActivityGraphUpdateMutation(\n $input: UpdateProcessingActivityInput!\n) {\n updateProcessingActivity(input: $input) {\n processingActivity {\n id\n name\n purpose\n dataSubjectCategory\n personalDataCategory\n specialOrCriminalData\n consentEvidenceLink\n lawfulBasis\n recipients\n location\n internationalTransfers\n transferSafeguards\n retentionPeriod\n securityMeasures\n dataProtectionImpactAssessment\n transferImpactAssessment\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n updatedAt\n }\n }\n}\n" } }; })(); -(node as any).hash = "ee0c2f6027edae2b80f603503f5f3815"; +(node as any).hash = "cb14a2c41f4690c0079a185c8caad3f4"; export default node; diff --git a/apps/console/src/pages/organizations/processingActivities/ProcessingActivityDetailsPage.tsx b/apps/console/src/pages/organizations/processingActivities/ProcessingActivityDetailsPage.tsx index c5e6f7f11..34e3e35c9 100644 --- a/apps/console/src/pages/organizations/processingActivities/ProcessingActivityDetailsPage.tsx +++ b/apps/console/src/pages/organizations/processingActivities/ProcessingActivityDetailsPage.tsx @@ -30,6 +30,7 @@ import { Controller } from "react-hook-form"; import z from "zod"; import { validateSnapshotConsistency } from "@probo/helpers"; import { SnapshotBanner } from "/components/SnapshotBanner"; +import { VendorsMultiSelectField } from "/components/form/VendorsMultiSelectField"; import { SpecialOrCriminalDataOptions, LawfulBasisOptions, @@ -56,6 +57,7 @@ const updateProcessingActivitySchema = z.object({ securityMeasures: z.string().optional(), dataProtectionImpactAssessment: z.enum(["NEEDED", "NOT_NEEDED"] as const), transferImpactAssessment: z.enum(["NEEDED", "NOT_NEEDED"] as const), + vendorIds: z.array(z.string()).optional(), }); type Props = { @@ -87,6 +89,9 @@ export default function ProcessingActivityDetailsPage(props: Props) { const deleteActivity = useDeleteProcessingActivity({ id: activity.id!, name: activity.name! }, connectionId); + const vendors = activity?.vendors?.edges.map((edge) => edge.node) ?? []; + const vendorIds = vendors.map((vendor) => vendor.id); + const { register, handleSubmit, formState, control } = useFormWithSchema( updateProcessingActivitySchema, { @@ -106,6 +111,7 @@ export default function ProcessingActivityDetailsPage(props: Props) { securityMeasures: activity.securityMeasures || "", dataProtectionImpactAssessment: activity.dataProtectionImpactAssessment || "NOT_NEEDED" as const, transferImpactAssessment: activity.transferImpactAssessment || "NOT_NEEDED" as const, + vendorIds: vendorIds, }, } ); @@ -129,6 +135,7 @@ export default function ProcessingActivityDetailsPage(props: Props) { securityMeasures: formData.securityMeasures || undefined, dataProtectionImpactAssessment: formData.dataProtectionImpactAssessment || undefined, transferImpactAssessment: formData.transferImpactAssessment || undefined, + vendorIds: formData.vendorIds, }); toast({ @@ -388,6 +395,15 @@ export default function ProcessingActivityDetailsPage(props: Props) { + + {!isSnapshotMode && (
+ + diff --git a/pkg/coredata/migrations/20251030T120000Z.sql b/pkg/coredata/migrations/20251030T120000Z.sql new file mode 100644 index 000000000..063ecdddb --- /dev/null +++ b/pkg/coredata/migrations/20251030T120000Z.sql @@ -0,0 +1,8 @@ +CREATE TABLE processing_activity_vendors ( + processing_activity_id TEXT REFERENCES processing_activities(id) ON DELETE CASCADE, + vendor_id TEXT REFERENCES vendors(id) ON DELETE CASCADE, + tenant_id TEXT NOT NULL, + snapshot_id TEXT, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + PRIMARY KEY (processing_activity_id, vendor_id) +); diff --git a/pkg/coredata/processing_activities.go b/pkg/coredata/processing_activities.go index 702688182..d3fae8cad 100644 --- a/pkg/coredata/processing_activities.go +++ b/pkg/coredata/processing_activities.go @@ -395,6 +395,24 @@ WHERE } func (pas ProcessingActivities) Snapshot(ctx context.Context, conn pg.Conn, scope Scoper, organizationID, snapshotID gid.GID) error { + snapshotters := []ProcessingActivitySnapshotter{ProcessingActivities{}, Vendors{}, ProcessingActivityVendors{}} + + for _, snapshotter := range snapshotters { + if err := snapshotter.InsertProcessingActivitySnapshots(ctx, conn, scope, organizationID, snapshotID); err != nil { + return fmt.Errorf("cannot create processing activity snapshots: (%T) %w", snapshotter, err) + } + } + + return nil +} + +func (pas ProcessingActivities) InsertProcessingActivitySnapshots( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, + snapshotID gid.GID, +) error { query := ` INSERT INTO processing_activities ( id, diff --git a/pkg/coredata/processing_activity_vendor.go b/pkg/coredata/processing_activity_vendor.go new file mode 100644 index 000000000..ed8f8ba7a --- /dev/null +++ b/pkg/coredata/processing_activity_vendor.go @@ -0,0 +1,177 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "context" + "fmt" + "maps" + "time" + + "github.com/getprobo/probo/pkg/gid" + "github.com/jackc/pgx/v5" + "go.gearno.de/kit/pg" +) + +type ( + ProcessingActivityVendor struct { + ProcessingActivityID gid.GID `db:"processing_activity_id"` + VendorID gid.GID `db:"vendor_id"` + TenantID gid.TenantID `db:"tenant_id"` + SnapshotID *gid.GID `db:"snapshot_id"` + CreatedAt time.Time `db:"created_at"` + } + + ProcessingActivityVendors []*ProcessingActivityVendor + + ProcessingActivitySnapshotter interface { + InsertProcessingActivitySnapshots(ctx context.Context, conn pg.Conn, scope Scoper, organizationID, snapshotID gid.GID) error + } +) + +func (pav ProcessingActivityVendors) Merge( + ctx context.Context, + conn pg.Conn, + scope Scoper, + processingActivityID gid.GID, + vendorIDs []gid.GID, +) error { + q := ` +WITH vendor_ids AS ( + SELECT + unnest(@vendor_ids::text[]) AS vendor_id, + @tenant_id AS tenant_id, + @processing_activity_id AS processing_activity_id, + @created_at::timestamptz AS created_at +) +MERGE INTO processing_activity_vendors AS tgt +USING vendor_ids AS src +ON tgt.tenant_id = src.tenant_id + AND tgt.processing_activity_id = src.processing_activity_id + AND tgt.vendor_id = src.vendor_id +WHEN NOT MATCHED + THEN INSERT (tenant_id, processing_activity_id, vendor_id, created_at) + VALUES (src.tenant_id, src.processing_activity_id, src.vendor_id, src.created_at) + WHEN NOT MATCHED BY SOURCE + AND tgt.tenant_id = @tenant_id AND tgt.processing_activity_id = @processing_activity_id + THEN DELETE + ` + + args := pgx.StrictNamedArgs{ + "tenant_id": scope.GetTenantID(), + "processing_activity_id": processingActivityID, + "created_at": time.Now(), + "vendor_ids": vendorIDs, + } + + _, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot merge processing activity vendors: %w", err) + } + + return nil +} + +func (pav ProcessingActivityVendors) Insert( + ctx context.Context, + conn pg.Conn, + scope Scoper, + processingActivityID gid.GID, + vendorIDs []gid.GID, +) error { + q := ` +WITH vendor_ids AS ( + SELECT unnest(@vendor_ids::text[]) AS vendor_id +) +INSERT INTO processing_activity_vendors (tenant_id, processing_activity_id, vendor_id, created_at) +SELECT + @tenant_id AS tenant_id, + @processing_activity_id AS processing_activity_id, + vendor_id, + @created_at AS created_at +FROM vendor_ids +` + + args := pgx.StrictNamedArgs{ + "tenant_id": scope.GetTenantID(), + "processing_activity_id": processingActivityID, + "created_at": time.Now(), + "vendor_ids": vendorIDs, + } + + _, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot insert processing activity vendors: %w", err) + } + + return nil +} + +func (pav ProcessingActivityVendors) InsertProcessingActivitySnapshots( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, + snapshotID gid.GID, +) error { + query := ` +WITH + source_processing_activities AS ( + SELECT id + FROM processing_activities + WHERE organization_id = @organization_id AND snapshot_id IS NULL + ), + snapshot_processing_activities AS ( + SELECT id, source_id + FROM processing_activities + WHERE organization_id = @organization_id AND snapshot_id = @snapshot_id + ), + snapshot_vendors AS ( + SELECT id, source_id + FROM vendors + WHERE organization_id = @organization_id AND snapshot_id = @snapshot_id + ), + source_processing_activity_vendors AS ( + SELECT processing_activity_id, vendor_id, snapshot_id, created_at + FROM processing_activity_vendors + WHERE %s AND processing_activity_id = ANY(SELECT id FROM source_processing_activities) AND snapshot_id IS NULL + ) +INSERT INTO processing_activity_vendors (tenant_id, processing_activity_id, vendor_id, snapshot_id, created_at) +SELECT + @tenant_id, + spa.id, + sv.id, + @snapshot_id, + pav.created_at +FROM source_processing_activity_vendors pav +JOIN snapshot_processing_activities spa ON spa.source_id = pav.processing_activity_id +JOIN snapshot_vendors sv ON sv.source_id = pav.vendor_id +` + + query = fmt.Sprintf(query, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "snapshot_id": snapshotID, + "organization_id": organizationID, + } + maps.Copy(args, scope.SQLArguments()) + + _, err := conn.Exec(ctx, query, args) + if err != nil { + return fmt.Errorf("cannot insert processing activity vendor snapshots: %w", err) + } + + return nil +} diff --git a/pkg/coredata/vendor.go b/pkg/coredata/vendor.go index eb1b97c66..030ad5942 100644 --- a/pkg/coredata/vendor.go +++ b/pkg/coredata/vendor.go @@ -731,6 +731,104 @@ WHERE %s return nil } +func (v *Vendors) LoadByProcessingActivityID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + processingActivityID gid.GID, + cursor *page.Cursor[VendorOrderField], +) error { + q := ` +WITH vend AS ( + SELECT + v.id, + v.tenant_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_id, + v.security_owner_id, + v.status_page_url, + v.terms_of_service_url, + v.security_page_url, + v.trust_page_url, + v.show_on_trust_center, + v.snapshot_id, + v.source_id, + v.created_at, + v.updated_at + FROM + vendors v + INNER JOIN + processing_activity_vendors pav ON v.id = pav.vendor_id + WHERE + pav.processing_activity_id = @processing_activity_id +) +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_id, + security_owner_id, + status_page_url, + terms_of_service_url, + security_page_url, + trust_page_url, + show_on_trust_center, + snapshot_id, + source_id, + created_at, + updated_at +FROM + vend +WHERE %s + AND %s +` + q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) + + args := pgx.StrictNamedArgs{"processing_activity_id": processingActivityID} + maps.Copy(args, scope.SQLArguments()) + maps.Copy(args, cursor.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 (d Vendors) InsertDataSnapshots( ctx context.Context, conn pg.Conn, @@ -935,6 +1033,108 @@ FROM source_vendors v return nil } +func (vs Vendors) InsertProcessingActivitySnapshots( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, + snapshotID gid.GID, +) error { + query := ` +WITH + source_processing_activities AS ( + SELECT id + FROM processing_activities + WHERE organization_id = @organization_id AND snapshot_id IS NULL + ), + source_processing_activity_vendors AS ( + SELECT processing_activity_id, vendor_id, snapshot_id, created_at + FROM processing_activity_vendors + WHERE processing_activity_id = ANY(SELECT id FROM source_processing_activities) + ), + source_vendors AS ( + SELECT * + FROM vendors + WHERE %s AND id = ANY(SELECT vendor_id FROM source_processing_activity_vendors) + ) +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_id, + security_owner_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_id, + v.security_owner_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 source_vendors v + ` + + 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 for processing activities: %w", err) + } + + return nil +} + func (v Vendors) Snapshot(ctx context.Context, conn pg.Conn, scope Scoper, organizationID, snapshotID gid.GID) error { for _, snapshotter := range []VendorSnapshotter{ Vendors{}, diff --git a/pkg/probo/processing_activity_service.go b/pkg/probo/processing_activity_service.go index 04a0b7f44..304fda974 100644 --- a/pkg/probo/processing_activity_service.go +++ b/pkg/probo/processing_activity_service.go @@ -47,6 +47,7 @@ type ( SecurityMeasures *string DataProtectionImpactAssessment coredata.ProcessingActivityDataProtectionImpactAssessment TransferImpactAssessment coredata.ProcessingActivityTransferImpactAssessment + VendorIDs []gid.GID } UpdateProcessingActivityRequest struct { @@ -66,6 +67,7 @@ type ( SecurityMeasures **string DataProtectionImpactAssessment *coredata.ProcessingActivityDataProtectionImpactAssessment TransferImpactAssessment *coredata.ProcessingActivityTransferImpactAssessment + VendorIDs *[]gid.GID } ) @@ -94,6 +96,7 @@ func (s *ProcessingActivityService) Create( req *CreateProcessingActivityRequest, ) (*coredata.ProcessingActivity, error) { now := time.Now() + processingActivityVendors := &coredata.ProcessingActivityVendors{} processingActivity := &coredata.ProcessingActivity{ ID: gid.New(s.svc.scope.GetTenantID(), coredata.ProcessingActivityEntityType), @@ -129,6 +132,12 @@ func (s *ProcessingActivityService) Create( return fmt.Errorf("cannot insert processing activity: %w", err) } + if len(req.VendorIDs) > 0 { + if err := processingActivityVendors.Insert(ctx, conn, s.svc.scope, processingActivity.ID, req.VendorIDs); err != nil { + return fmt.Errorf("cannot create processing activity vendors: %w", err) + } + } + return nil }, ) @@ -145,6 +154,7 @@ func (s *ProcessingActivityService) Update( req *UpdateProcessingActivityRequest, ) (*coredata.ProcessingActivity, error) { processingActivity := &coredata.ProcessingActivity{} + processingActivityVendors := &coredata.ProcessingActivityVendors{} err := s.svc.pg.WithTx( ctx, @@ -205,6 +215,12 @@ func (s *ProcessingActivityService) Update( return fmt.Errorf("cannot update processing activity: %w", err) } + if req.VendorIDs != nil { + if err := processingActivityVendors.Merge(ctx, conn, s.svc.scope, processingActivity.ID, *req.VendorIDs); err != nil { + return fmt.Errorf("cannot update processing activity vendors: %w", err) + } + } + return nil }, ) diff --git a/pkg/probo/vendor_service.go b/pkg/probo/vendor_service.go index 52bdbaddc..ea6548e5e 100644 --- a/pkg/probo/vendor_service.go +++ b/pkg/probo/vendor_service.go @@ -491,6 +491,32 @@ func (s VendorService) ListForAssetID( return page.NewPage(vendors, cursor), nil } +func (s VendorService) ListForProcessingActivityID( + ctx context.Context, + processingActivityID gid.GID, + cursor *page.Cursor[coredata.VendorOrderField], +) (*page.Page[*coredata.Vendor, coredata.VendorOrderField], error) { + var vendors coredata.Vendors + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + err := vendors.LoadByProcessingActivityID(ctx, conn, s.svc.scope, processingActivityID, cursor) + if err != nil { + return fmt.Errorf("cannot load vendors by processing activity: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return page.NewPage(vendors, cursor), nil +} + func (s VendorService) ListRiskAssessments( ctx context.Context, vendorID gid.GID, diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index edc655e3f..a33d3410d 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -2358,6 +2358,13 @@ type ProcessingActivity implements Node { securityMeasures: String dataProtectionImpactAssessment: ProcessingActivityDataProtectionImpactAssessment! transferImpactAssessment: ProcessingActivityTransferImpactAssessment! + vendors( + first: Int + after: CursorKey + last: Int + before: CursorKey + orderBy: VendorOrder + ): VendorConnection! @goField(forceResolver: true) createdAt: Datetime! updatedAt: Datetime! } @@ -3924,6 +3931,7 @@ input CreateProcessingActivityInput { securityMeasures: String dataProtectionImpactAssessment: ProcessingActivityDataProtectionImpactAssessment! transferImpactAssessment: ProcessingActivityTransferImpactAssessment! + vendorIds: [ID!] } input UpdateProcessingActivityInput { @@ -3944,6 +3952,7 @@ input UpdateProcessingActivityInput { securityMeasures: String @goField(omittable: true) dataProtectionImpactAssessment: ProcessingActivityDataProtectionImpactAssessment transferImpactAssessment: ProcessingActivityTransferImpactAssessment + vendorIds: [ID!] } input DeleteProcessingActivityInput { diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index 8af6c919c..4e03e61f3 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -1149,6 +1149,7 @@ type ComplexityRoot struct { TransferImpactAssessment func(childComplexity int) int TransferSafeguards func(childComplexity int) int UpdatedAt func(childComplexity int) int + Vendors func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy) int } ProcessingActivityConnection struct { @@ -2046,6 +2047,8 @@ type PeopleConnectionResolver interface { } type ProcessingActivityResolver interface { Organization(ctx context.Context, obj *types.ProcessingActivity) (*types.Organization, error) + + Vendors(ctx context.Context, obj *types.ProcessingActivity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy) (*types.VendorConnection, error) } type ProcessingActivityConnectionResolver interface { TotalCount(ctx context.Context, obj *types.ProcessingActivityConnection) (int, error) @@ -7103,6 +7106,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.ProcessingActivity.UpdatedAt(childComplexity), true + case "ProcessingActivity.vendors": + if e.complexity.ProcessingActivity.Vendors == nil { + break + } + + args, err := ec.field_ProcessingActivity_vendors_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.ProcessingActivity.Vendors(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.VendorOrderBy)), true + case "ProcessingActivityConnection.edges": if e.complexity.ProcessingActivityConnection.Edges == nil { break @@ -12084,6 +12099,13 @@ type ProcessingActivity implements Node { securityMeasures: String dataProtectionImpactAssessment: ProcessingActivityDataProtectionImpactAssessment! transferImpactAssessment: ProcessingActivityTransferImpactAssessment! + vendors( + first: Int + after: CursorKey + last: Int + before: CursorKey + orderBy: VendorOrder + ): VendorConnection! @goField(forceResolver: true) createdAt: Datetime! updatedAt: Datetime! } @@ -13650,6 +13672,7 @@ input CreateProcessingActivityInput { securityMeasures: String dataProtectionImpactAssessment: ProcessingActivityDataProtectionImpactAssessment! transferImpactAssessment: ProcessingActivityTransferImpactAssessment! + vendorIds: [ID!] } input UpdateProcessingActivityInput { @@ -13670,6 +13693,7 @@ input UpdateProcessingActivityInput { securityMeasures: String @goField(omittable: true) dataProtectionImpactAssessment: ProcessingActivityDataProtectionImpactAssessment transferImpactAssessment: ProcessingActivityTransferImpactAssessment + vendorIds: [ID!] } input DeleteProcessingActivityInput { @@ -21453,6 +21477,101 @@ func (ec *executionContext) field_Organization_vendors_argsFilter( return zeroVal, nil } +func (ec *executionContext) field_ProcessingActivity_vendors_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_ProcessingActivity_vendors_argsFirst(ctx, rawArgs) + if err != nil { + return nil, err + } + args["first"] = arg0 + arg1, err := ec.field_ProcessingActivity_vendors_argsAfter(ctx, rawArgs) + if err != nil { + return nil, err + } + args["after"] = arg1 + arg2, err := ec.field_ProcessingActivity_vendors_argsLast(ctx, rawArgs) + if err != nil { + return nil, err + } + args["last"] = arg2 + arg3, err := ec.field_ProcessingActivity_vendors_argsBefore(ctx, rawArgs) + if err != nil { + return nil, err + } + args["before"] = arg3 + arg4, err := ec.field_ProcessingActivity_vendors_argsOrderBy(ctx, rawArgs) + if err != nil { + return nil, err + } + args["orderBy"] = arg4 + return args, nil +} +func (ec *executionContext) field_ProcessingActivity_vendors_argsFirst( + ctx context.Context, + rawArgs map[string]any, +) (*int, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("first")) + if tmp, ok := rawArgs["first"]; ok { + return ec.unmarshalOInt2ᚖint(ctx, tmp) + } + + var zeroVal *int + return zeroVal, nil +} + +func (ec *executionContext) field_ProcessingActivity_vendors_argsAfter( + ctx context.Context, + rawArgs map[string]any, +) (*page.CursorKey, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) + if tmp, ok := rawArgs["after"]; ok { + return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) + } + + var zeroVal *page.CursorKey + return zeroVal, nil +} + +func (ec *executionContext) field_ProcessingActivity_vendors_argsLast( + ctx context.Context, + rawArgs map[string]any, +) (*int, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("last")) + if tmp, ok := rawArgs["last"]; ok { + return ec.unmarshalOInt2ᚖint(ctx, tmp) + } + + var zeroVal *int + return zeroVal, nil +} + +func (ec *executionContext) field_ProcessingActivity_vendors_argsBefore( + ctx context.Context, + rawArgs map[string]any, +) (*page.CursorKey, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) + if tmp, ok := rawArgs["before"]; ok { + return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) + } + + var zeroVal *page.CursorKey + return zeroVal, nil +} + +func (ec *executionContext) field_ProcessingActivity_vendors_argsOrderBy( + ctx context.Context, + rawArgs map[string]any, +) (*types.VendorOrderBy, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("orderBy")) + if tmp, ok := rawArgs["orderBy"]; ok { + return ec.unmarshalOVendorOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐVendorOrderBy(ctx, tmp) + } + + var zeroVal *types.VendorOrderBy + return zeroVal, nil +} + func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -53757,6 +53876,69 @@ func (ec *executionContext) fieldContext_ProcessingActivity_transferImpactAssess return fc, nil } +func (ec *executionContext) _ProcessingActivity_vendors(ctx context.Context, field graphql.CollectedField, obj *types.ProcessingActivity) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ProcessingActivity_vendors(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.ProcessingActivity().Vendors(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.VendorOrderBy)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*types.VendorConnection) + fc.Result = res + return ec.marshalNVendorConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐVendorConnection(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ProcessingActivity_vendors(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ProcessingActivity", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "totalCount": + return ec.fieldContext_VendorConnection_totalCount(ctx, field) + case "edges": + return ec.fieldContext_VendorConnection_edges(ctx, field) + case "pageInfo": + return ec.fieldContext_VendorConnection_pageInfo(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type VendorConnection", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_ProcessingActivity_vendors_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _ProcessingActivity_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.ProcessingActivity) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ProcessingActivity_createdAt(ctx, field) if err != nil { @@ -54114,6 +54296,8 @@ func (ec *executionContext) fieldContext_ProcessingActivityEdge_node(_ context.C return ec.fieldContext_ProcessingActivity_dataProtectionImpactAssessment(ctx, field) case "transferImpactAssessment": return ec.fieldContext_ProcessingActivity_transferImpactAssessment(ctx, field) + case "vendors": + return ec.fieldContext_ProcessingActivity_vendors(ctx, field) case "createdAt": return ec.fieldContext_ProcessingActivity_createdAt(ctx, field) case "updatedAt": @@ -63933,6 +64117,8 @@ func (ec *executionContext) fieldContext_UpdateProcessingActivityPayload_process return ec.fieldContext_ProcessingActivity_dataProtectionImpactAssessment(ctx, field) case "transferImpactAssessment": return ec.fieldContext_ProcessingActivity_transferImpactAssessment(ctx, field) + case "vendors": + return ec.fieldContext_ProcessingActivity_vendors(ctx, field) case "createdAt": return ec.fieldContext_ProcessingActivity_createdAt(ctx, field) case "updatedAt": @@ -74582,7 +74768,7 @@ func (ec *executionContext) unmarshalInputCreateProcessingActivityInput(ctx cont asMap[k] = v } - fieldsInOrder := [...]string{"organizationId", "name", "purpose", "dataSubjectCategory", "personalDataCategory", "specialOrCriminalData", "consentEvidenceLink", "lawfulBasis", "recipients", "location", "internationalTransfers", "transferSafeguards", "retentionPeriod", "securityMeasures", "dataProtectionImpactAssessment", "transferImpactAssessment"} + fieldsInOrder := [...]string{"organizationId", "name", "purpose", "dataSubjectCategory", "personalDataCategory", "specialOrCriminalData", "consentEvidenceLink", "lawfulBasis", "recipients", "location", "internationalTransfers", "transferSafeguards", "retentionPeriod", "securityMeasures", "dataProtectionImpactAssessment", "transferImpactAssessment", "vendorIds"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -74701,6 +74887,13 @@ func (ec *executionContext) unmarshalInputCreateProcessingActivityInput(ctx cont return it, err } it.TransferImpactAssessment = data + case "vendorIds": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("vendorIds")) + data, err := ec.unmarshalOID2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGIDᚄ(ctx, v) + if err != nil { + return it, err + } + it.VendorIds = data } } @@ -79266,7 +79459,7 @@ func (ec *executionContext) unmarshalInputUpdateProcessingActivityInput(ctx cont asMap[k] = v } - fieldsInOrder := [...]string{"id", "name", "purpose", "dataSubjectCategory", "personalDataCategory", "specialOrCriminalData", "consentEvidenceLink", "lawfulBasis", "recipients", "location", "internationalTransfers", "transferSafeguards", "retentionPeriod", "securityMeasures", "dataProtectionImpactAssessment", "transferImpactAssessment"} + fieldsInOrder := [...]string{"id", "name", "purpose", "dataSubjectCategory", "personalDataCategory", "specialOrCriminalData", "consentEvidenceLink", "lawfulBasis", "recipients", "location", "internationalTransfers", "transferSafeguards", "retentionPeriod", "securityMeasures", "dataProtectionImpactAssessment", "transferImpactAssessment", "vendorIds"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -79385,6 +79578,13 @@ func (ec *executionContext) unmarshalInputUpdateProcessingActivityInput(ctx cont return it, err } it.TransferImpactAssessment = data + case "vendorIds": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("vendorIds")) + data, err := ec.unmarshalOID2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGIDᚄ(ctx, v) + if err != nil { + return it, err + } + it.VendorIds = data } } @@ -91809,6 +92009,42 @@ func (ec *executionContext) _ProcessingActivity(ctx context.Context, sel ast.Sel if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "vendors": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ProcessingActivity_vendors(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "createdAt": out.Values[i] = ec._ProcessingActivity_createdAt(ctx, field, obj) if out.Values[i] == graphql.Null { diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index 449d561be..fd880b5f7 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -451,6 +451,7 @@ type CreateProcessingActivityInput struct { SecurityMeasures *string `json:"securityMeasures,omitempty"` DataProtectionImpactAssessment coredata.ProcessingActivityDataProtectionImpactAssessment `json:"dataProtectionImpactAssessment"` TransferImpactAssessment coredata.ProcessingActivityTransferImpactAssessment `json:"transferImpactAssessment"` + VendorIds []gid.GID `json:"vendorIds,omitempty"` } type CreateProcessingActivityPayload struct { @@ -1528,6 +1529,7 @@ type ProcessingActivity struct { SecurityMeasures *string `json:"securityMeasures,omitempty"` DataProtectionImpactAssessment coredata.ProcessingActivityDataProtectionImpactAssessment `json:"dataProtectionImpactAssessment"` TransferImpactAssessment coredata.ProcessingActivityTransferImpactAssessment `json:"transferImpactAssessment"` + Vendors *VendorConnection `json:"vendors"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } @@ -2036,6 +2038,7 @@ type UpdateProcessingActivityInput struct { SecurityMeasures graphql.Omittable[*string] `json:"securityMeasures,omitempty"` DataProtectionImpactAssessment *coredata.ProcessingActivityDataProtectionImpactAssessment `json:"dataProtectionImpactAssessment,omitempty"` TransferImpactAssessment *coredata.ProcessingActivityTransferImpactAssessment `json:"transferImpactAssessment,omitempty"` + VendorIds []gid.GID `json:"vendorIds,omitempty"` } type UpdateProcessingActivityPayload struct { diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 13ff7be26..9cbb377e7 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -3405,6 +3405,7 @@ func (r *mutationResolver) CreateProcessingActivity(ctx context.Context, input t SecurityMeasures: input.SecurityMeasures, DataProtectionImpactAssessment: input.DataProtectionImpactAssessment, TransferImpactAssessment: input.TransferImpactAssessment, + VendorIDs: input.VendorIds, } activity, err := prb.ProcessingActivities.Create(ctx, &req) @@ -3437,6 +3438,7 @@ func (r *mutationResolver) UpdateProcessingActivity(ctx context.Context, input t SecurityMeasures: UnwrapOmittable(input.SecurityMeasures), DataProtectionImpactAssessment: input.DataProtectionImpactAssessment, TransferImpactAssessment: input.TransferImpactAssessment, + VendorIDs: &input.VendorIds, } activity, err := prb.ProcessingActivities.Update(ctx, &req) @@ -4556,6 +4558,31 @@ func (r *processingActivityResolver) Organization(ctx context.Context, obj *type return types.NewOrganization(organization), nil } +// Vendors is the resolver for the vendors field. +func (r *processingActivityResolver) Vendors(ctx context.Context, obj *types.ProcessingActivity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy) (*types.VendorConnection, error) { + prb := r.ProboService(ctx, obj.ID.TenantID()) + + pageOrderBy := page.OrderBy[coredata.VendorOrderField]{ + Field: coredata.VendorOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if orderBy != nil { + pageOrderBy = page.OrderBy[coredata.VendorOrderField]{ + Field: orderBy.Field, + Direction: orderBy.Direction, + } + } + + cursor := types.NewCursor(first, after, last, before, pageOrderBy) + + page, err := prb.Vendors.ListForProcessingActivityID(ctx, obj.ID, cursor) + if err != nil { + panic(fmt.Errorf("cannot list processing activity vendors: %w", err)) + } + + return types.NewVendorConnection(page, r, obj.ID), nil +} + // TotalCount is the resolver for the totalCount field. func (r *processingActivityConnectionResolver) TotalCount(ctx context.Context, obj *types.ProcessingActivityConnection) (int, error) { prb := r.ProboService(ctx, obj.ParentID.TenantID())