From 4f6884cb8ff25294f779661a75d3b4ba3fabb3fd Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Mon, 9 Feb 2026 14:04:51 +0100 Subject: [PATCH] Fix duplicate assessments Signed-off-by: Sacha Al Himdani --- ...ActivityGraphCreateDPIAMutation.graphql.ts | 202 +++++++++++++----- ...gActivityGraphCreateTIAMutation.graphql.ts | 202 +++++++++++++----- .../hooks/graph/ProcessingActivityGraph.ts | 50 +++++ .../ProcessingActivityDetailsPage.tsx | 2 + .../data_protection_impact_assessment.go | 8 + pkg/coredata/migrations/20260209T125649Z.sql | 33 +++ pkg/coredata/transfer_impact_assessment.go | 8 + pkg/server/api/console/v1/v1_resolver.go | 14 +- 8 files changed, 401 insertions(+), 118 deletions(-) create mode 100644 pkg/coredata/migrations/20260209T125649Z.sql diff --git a/apps/console/src/__generated__/core/ProcessingActivityGraphCreateDPIAMutation.graphql.ts b/apps/console/src/__generated__/core/ProcessingActivityGraphCreateDPIAMutation.graphql.ts index 0c517b5c4..b75c45c65 100644 --- a/apps/console/src/__generated__/core/ProcessingActivityGraphCreateDPIAMutation.graphql.ts +++ b/apps/console/src/__generated__/core/ProcessingActivityGraphCreateDPIAMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<85c085bec1ebdad122bbf166590c3788>> * @lightSyntaxTransform * @nogrep */ @@ -24,12 +24,29 @@ export type ProcessingActivityGraphCreateDPIAMutation$variables = { export type ProcessingActivityGraphCreateDPIAMutation$data = { readonly createDataProtectionImpactAssessment: { readonly dataProtectionImpactAssessment: { + readonly canDelete: boolean; + readonly canUpdate: boolean; readonly createdAt: string; readonly description: string | null | undefined; readonly id: string; readonly mitigations: string | null | undefined; readonly necessityAndProportionality: string | null | undefined; readonly potentialRisk: string | null | undefined; + readonly processingActivity: { + readonly dataProtectionImpactAssessment: { + readonly canDelete: boolean; + readonly canUpdate: boolean; + readonly createdAt: string; + readonly description: string | null | undefined; + readonly id: string; + readonly mitigations: string | null | undefined; + readonly necessityAndProportionality: string | null | undefined; + readonly potentialRisk: string | null | undefined; + readonly residualRisk: DataProtectionImpactAssessmentResidualRisk | null | undefined; + readonly updatedAt: string; + } | null | undefined; + readonly id: string; + }; readonly residualRisk: DataProtectionImpactAssessmentResidualRisk | null | undefined; readonly updatedAt: string; }; @@ -48,7 +65,89 @@ var v0 = [ "name": "input" } ], -v1 = [ +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "necessityAndProportionality", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "potentialRisk", + "storageKey": null +}, +v5 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "mitigations", + "storageKey": null +}, +v6 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "residualRisk", + "storageKey": null +}, +v7 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null +}, +v8 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null +}, +v9 = { + "alias": "canUpdate", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:data-protection-impact-assessment:update" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"core:data-protection-impact-assessment:update\")" +}, +v10 = { + "alias": "canDelete", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:data-protection-impact-assessment:delete" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"core:data-protection-impact-assessment:delete\")" +}, +v11 = [ { "alias": null, "args": [ @@ -71,60 +170,47 @@ v1 = [ "name": "dataProtectionImpactAssessment", "plural": false, "selections": [ + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v6/*: any*/), + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), { "alias": null, "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "description", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "necessityAndProportionality", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "potentialRisk", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "mitigations", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "residualRisk", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "createdAt", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "updatedAt", + "concreteType": "ProcessingActivity", + "kind": "LinkedField", + "name": "processingActivity", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "DataProtectionImpactAssessment", + "kind": "LinkedField", + "name": "dataProtectionImpactAssessment", + "plural": false, + "selections": [ + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v6/*: any*/), + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/) + ], + "storageKey": null + } + ], "storageKey": null } ], @@ -140,7 +226,7 @@ return { "kind": "Fragment", "metadata": null, "name": "ProcessingActivityGraphCreateDPIAMutation", - "selections": (v1/*: any*/), + "selections": (v11/*: any*/), "type": "Mutation", "abstractKey": null }, @@ -149,19 +235,19 @@ return { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", "name": "ProcessingActivityGraphCreateDPIAMutation", - "selections": (v1/*: any*/) + "selections": (v11/*: any*/) }, "params": { - "cacheID": "71220501284ee2a9761f87d8909fcd00", + "cacheID": "ea459920c052d1188c193c0a7f7bf4bc", "id": null, "metadata": {}, "name": "ProcessingActivityGraphCreateDPIAMutation", "operationKind": "mutation", - "text": "mutation ProcessingActivityGraphCreateDPIAMutation(\n $input: CreateDataProtectionImpactAssessmentInput!\n) {\n createDataProtectionImpactAssessment(input: $input) {\n dataProtectionImpactAssessment {\n id\n description\n necessityAndProportionality\n potentialRisk\n mitigations\n residualRisk\n createdAt\n updatedAt\n }\n }\n}\n" + "text": "mutation ProcessingActivityGraphCreateDPIAMutation(\n $input: CreateDataProtectionImpactAssessmentInput!\n) {\n createDataProtectionImpactAssessment(input: $input) {\n dataProtectionImpactAssessment {\n id\n description\n necessityAndProportionality\n potentialRisk\n mitigations\n residualRisk\n createdAt\n updatedAt\n canUpdate: permission(action: \"core:data-protection-impact-assessment:update\")\n canDelete: permission(action: \"core:data-protection-impact-assessment:delete\")\n processingActivity {\n id\n dataProtectionImpactAssessment {\n id\n description\n necessityAndProportionality\n potentialRisk\n mitigations\n residualRisk\n createdAt\n updatedAt\n canUpdate: permission(action: \"core:data-protection-impact-assessment:update\")\n canDelete: permission(action: \"core:data-protection-impact-assessment:delete\")\n }\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "e7c379b7972e44c5c854cb4fd486f557"; +(node as any).hash = "f9a989390460f5567e3661fd7e757f66"; export default node; diff --git a/apps/console/src/__generated__/core/ProcessingActivityGraphCreateTIAMutation.graphql.ts b/apps/console/src/__generated__/core/ProcessingActivityGraphCreateTIAMutation.graphql.ts index e41582114..1cc0a727a 100644 --- a/apps/console/src/__generated__/core/ProcessingActivityGraphCreateTIAMutation.graphql.ts +++ b/apps/console/src/__generated__/core/ProcessingActivityGraphCreateTIAMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<540f500784ebd0386ced3eb31a6afa49>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -23,11 +23,28 @@ export type ProcessingActivityGraphCreateTIAMutation$variables = { export type ProcessingActivityGraphCreateTIAMutation$data = { readonly createTransferImpactAssessment: { readonly transferImpactAssessment: { + readonly canDelete: boolean; + readonly canUpdate: boolean; readonly createdAt: string; readonly dataSubjects: string | null | undefined; readonly id: string; readonly legalMechanism: string | null | undefined; readonly localLawRisk: string | null | undefined; + readonly processingActivity: { + readonly id: string; + readonly transferImpactAssessment: { + readonly canDelete: boolean; + readonly canUpdate: boolean; + readonly createdAt: string; + readonly dataSubjects: string | null | undefined; + readonly id: string; + readonly legalMechanism: string | null | undefined; + readonly localLawRisk: string | null | undefined; + readonly supplementaryMeasures: string | null | undefined; + readonly transfer: string | null | undefined; + readonly updatedAt: string; + } | null | undefined; + }; readonly supplementaryMeasures: string | null | undefined; readonly transfer: string | null | undefined; readonly updatedAt: string; @@ -47,7 +64,89 @@ var v0 = [ "name": "input" } ], -v1 = [ +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "dataSubjects", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "legalMechanism", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "transfer", + "storageKey": null +}, +v5 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "localLawRisk", + "storageKey": null +}, +v6 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "supplementaryMeasures", + "storageKey": null +}, +v7 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null +}, +v8 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null +}, +v9 = { + "alias": "canUpdate", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:transfer-impact-assessment:update" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"core:transfer-impact-assessment:update\")" +}, +v10 = { + "alias": "canDelete", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:transfer-impact-assessment:delete" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"core:transfer-impact-assessment:delete\")" +}, +v11 = [ { "alias": null, "args": [ @@ -70,60 +169,47 @@ v1 = [ "name": "transferImpactAssessment", "plural": false, "selections": [ + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v6/*: any*/), + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), { "alias": null, "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "dataSubjects", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "legalMechanism", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "transfer", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "localLawRisk", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "supplementaryMeasures", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "createdAt", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "updatedAt", + "concreteType": "ProcessingActivity", + "kind": "LinkedField", + "name": "processingActivity", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "TransferImpactAssessment", + "kind": "LinkedField", + "name": "transferImpactAssessment", + "plural": false, + "selections": [ + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v6/*: any*/), + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/) + ], + "storageKey": null + } + ], "storageKey": null } ], @@ -139,7 +225,7 @@ return { "kind": "Fragment", "metadata": null, "name": "ProcessingActivityGraphCreateTIAMutation", - "selections": (v1/*: any*/), + "selections": (v11/*: any*/), "type": "Mutation", "abstractKey": null }, @@ -148,19 +234,19 @@ return { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", "name": "ProcessingActivityGraphCreateTIAMutation", - "selections": (v1/*: any*/) + "selections": (v11/*: any*/) }, "params": { - "cacheID": "70e30255cf15b77fd95a57d6ddd50c97", + "cacheID": "f8ebabea46a0088051fd6f379db8b729", "id": null, "metadata": {}, "name": "ProcessingActivityGraphCreateTIAMutation", "operationKind": "mutation", - "text": "mutation ProcessingActivityGraphCreateTIAMutation(\n $input: CreateTransferImpactAssessmentInput!\n) {\n createTransferImpactAssessment(input: $input) {\n transferImpactAssessment {\n id\n dataSubjects\n legalMechanism\n transfer\n localLawRisk\n supplementaryMeasures\n createdAt\n updatedAt\n }\n }\n}\n" + "text": "mutation ProcessingActivityGraphCreateTIAMutation(\n $input: CreateTransferImpactAssessmentInput!\n) {\n createTransferImpactAssessment(input: $input) {\n transferImpactAssessment {\n id\n dataSubjects\n legalMechanism\n transfer\n localLawRisk\n supplementaryMeasures\n createdAt\n updatedAt\n canUpdate: permission(action: \"core:transfer-impact-assessment:update\")\n canDelete: permission(action: \"core:transfer-impact-assessment:delete\")\n processingActivity {\n id\n transferImpactAssessment {\n id\n dataSubjects\n legalMechanism\n transfer\n localLawRisk\n supplementaryMeasures\n createdAt\n updatedAt\n canUpdate: permission(action: \"core:transfer-impact-assessment:update\")\n canDelete: permission(action: \"core:transfer-impact-assessment:delete\")\n }\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "43e9f83c7abb5d6f4d2a394c05940fbd"; +(node as any).hash = "b4bca88fcf13792cf8d03ba91de4261c"; export default node; diff --git a/apps/console/src/hooks/graph/ProcessingActivityGraph.ts b/apps/console/src/hooks/graph/ProcessingActivityGraph.ts index 4f782b36b..6b155a5ab 100644 --- a/apps/console/src/hooks/graph/ProcessingActivityGraph.ts +++ b/apps/console/src/hooks/graph/ProcessingActivityGraph.ts @@ -389,6 +389,31 @@ export const createDataProtectionImpactAssessmentMutation = graphql` residualRisk createdAt updatedAt + canUpdate: permission( + action: "core:data-protection-impact-assessment:update" + ) + canDelete: permission( + action: "core:data-protection-impact-assessment:delete" + ) + processingActivity { + id + dataProtectionImpactAssessment { + id + description + necessityAndProportionality + potentialRisk + mitigations + residualRisk + createdAt + updatedAt + canUpdate: permission( + action: "core:data-protection-impact-assessment:update" + ) + canDelete: permission( + action: "core:data-protection-impact-assessment:delete" + ) + } + } } } } @@ -521,6 +546,31 @@ export const createTransferImpactAssessmentMutation = graphql` supplementaryMeasures createdAt updatedAt + canUpdate: permission( + action: "core:transfer-impact-assessment:update" + ) + canDelete: permission( + action: "core:transfer-impact-assessment:delete" + ) + processingActivity { + id + transferImpactAssessment { + id + dataSubjects + legalMechanism + transfer + localLawRisk + supplementaryMeasures + createdAt + updatedAt + canUpdate: permission( + action: "core:transfer-impact-assessment:update" + ) + canDelete: permission( + action: "core:transfer-impact-assessment:delete" + ) + } + } } } } diff --git a/apps/console/src/pages/organizations/processingActivities/ProcessingActivityDetailsPage.tsx b/apps/console/src/pages/organizations/processingActivities/ProcessingActivityDetailsPage.tsx index 2704278d9..9078c722b 100644 --- a/apps/console/src/pages/organizations/processingActivities/ProcessingActivityDetailsPage.tsx +++ b/apps/console/src/pages/organizations/processingActivities/ProcessingActivityDetailsPage.tsx @@ -338,6 +338,7 @@ export default function ProcessingActivityDetailsPage(props: Props) { || undefined, }); setDpiaDeleted(false); + setShowDpiaForm(true); toast({ title: __("Success"), description: __("DPIA created successfully"), @@ -388,6 +389,7 @@ export default function ProcessingActivityDetailsPage(props: Props) { supplementaryMeasures: formData.supplementaryMeasures || undefined, }); setTiaDeleted(false); + setShowTiaForm(true); toast({ title: __("Success"), description: __("TIA created successfully"), diff --git a/pkg/coredata/data_protection_impact_assessment.go b/pkg/coredata/data_protection_impact_assessment.go index 29b814bfc..2d7336d59 100644 --- a/pkg/coredata/data_protection_impact_assessment.go +++ b/pkg/coredata/data_protection_impact_assessment.go @@ -22,6 +22,7 @@ import ( "time" "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" "go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/page" @@ -361,6 +362,13 @@ INSERT INTO processing_activity_data_protection_impact_assessments ( _, err := conn.Exec(ctx, q, args) if err != nil { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + if pgErr.Code == "23505" && pgErr.ConstraintName == "processing_activity_dpias_processing_activity_id_snapshot_id_uniq" { + return ErrResourceAlreadyExists + } + } + return fmt.Errorf("cannot insert data protection impact assessment: %w", err) } diff --git a/pkg/coredata/migrations/20260209T125649Z.sql b/pkg/coredata/migrations/20260209T125649Z.sql new file mode 100644 index 000000000..a0fb47e5e --- /dev/null +++ b/pkg/coredata/migrations/20260209T125649Z.sql @@ -0,0 +1,33 @@ +DELETE FROM processing_activity_data_protection_impact_assessments +WHERE id IN ( + SELECT id + FROM ( + SELECT id, + ROW_NUMBER() OVER ( + PARTITION BY processing_activity_id, COALESCE(snapshot_id, '') + ORDER BY updated_at DESC + ) AS rn + FROM processing_activity_data_protection_impact_assessments + ) t + WHERE rn > 1 +); + +DELETE FROM processing_activity_transfer_impact_assessments +WHERE id IN ( + SELECT id + FROM ( + SELECT id, + ROW_NUMBER() OVER ( + PARTITION BY processing_activity_id, COALESCE(snapshot_id, '') + ORDER BY updated_at DESC + ) AS rn + FROM processing_activity_transfer_impact_assessments + ) t + WHERE rn > 1 +); + +CREATE UNIQUE INDEX processing_activity_dpias_processing_activity_id_snapshot_id_uniq + ON processing_activity_data_protection_impact_assessments (processing_activity_id, COALESCE(snapshot_id, '')); + +CREATE UNIQUE INDEX processing_activity_tias_processing_activity_id_snapshot_id_uniq + ON processing_activity_transfer_impact_assessments (processing_activity_id, COALESCE(snapshot_id, '')); diff --git a/pkg/coredata/transfer_impact_assessment.go b/pkg/coredata/transfer_impact_assessment.go index af1de2264..9b8bc19c1 100644 --- a/pkg/coredata/transfer_impact_assessment.go +++ b/pkg/coredata/transfer_impact_assessment.go @@ -22,6 +22,7 @@ import ( "time" "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" "go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/page" @@ -359,6 +360,13 @@ INSERT INTO processing_activity_transfer_impact_assessments ( _, err := conn.Exec(ctx, q, args) if err != nil { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + if pgErr.Code == "23505" && pgErr.ConstraintName == "processing_activity_tias_processing_activity_id_snapshot_id_uniq" { + return ErrResourceAlreadyExists + } + } + return fmt.Errorf("cannot insert transfer impact assessment: %w", err) } diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 3362db716..0586c2bb2 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -5141,7 +5141,12 @@ func (r *mutationResolver) CreateDataProtectionImpactAssessment(ctx context.Cont dpia, err := prb.DataProtectionImpactAssessments.Create(ctx, &req) if err != nil { - panic(fmt.Errorf("cannot create data protection impact assessment: %w", err)) + if errors.Is(err, coredata.ErrResourceAlreadyExists) { + return nil, gqlutils.Conflict(ctx, err) + } + + r.logger.ErrorCtx(ctx, "cannot create data protection impact assessment", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return &types.CreateDataProtectionImpactAssessmentPayload{ @@ -5213,7 +5218,12 @@ func (r *mutationResolver) CreateTransferImpactAssessment(ctx context.Context, i tia, err := prb.TransferImpactAssessments.Create(ctx, &req) if err != nil { - panic(fmt.Errorf("cannot create transfer impact assessment: %w", err)) + if errors.Is(err, coredata.ErrResourceAlreadyExists) { + return nil, gqlutils.Conflict(ctx, err) + } + + r.logger.ErrorCtx(ctx, "cannot create transfer impact assessment", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return &types.CreateTransferImpactAssessmentPayload{