Fix duplicate assessments

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-02-09 14:04:51 +01:00
parent 99f1da1560
commit 4f6884cb8f
8 changed files with 401 additions and 118 deletions

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<d993bbef933d660af5aaac0a88d3c7f7>> * @generated SignedSource<<85c085bec1ebdad122bbf166590c3788>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -24,12 +24,29 @@ export type ProcessingActivityGraphCreateDPIAMutation$variables = {
export type ProcessingActivityGraphCreateDPIAMutation$data = { export type ProcessingActivityGraphCreateDPIAMutation$data = {
readonly createDataProtectionImpactAssessment: { readonly createDataProtectionImpactAssessment: {
readonly dataProtectionImpactAssessment: { readonly dataProtectionImpactAssessment: {
readonly canDelete: boolean;
readonly canUpdate: boolean;
readonly createdAt: string; readonly createdAt: string;
readonly description: string | null | undefined; readonly description: string | null | undefined;
readonly id: string; readonly id: string;
readonly mitigations: string | null | undefined; readonly mitigations: string | null | undefined;
readonly necessityAndProportionality: string | null | undefined; readonly necessityAndProportionality: string | null | undefined;
readonly potentialRisk: 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 residualRisk: DataProtectionImpactAssessmentResidualRisk | null | undefined;
readonly updatedAt: string; readonly updatedAt: string;
}; };
@@ -48,7 +65,89 @@ var v0 = [
"name": "input" "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, "alias": null,
"args": [ "args": [
@@ -71,60 +170,47 @@ v1 = [
"name": "dataProtectionImpactAssessment", "name": "dataProtectionImpactAssessment",
"plural": false, "plural": false,
"selections": [ "selections": [
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "concreteType": "ProcessingActivity",
"name": "id", "kind": "LinkedField",
"storageKey": null "name": "processingActivity",
}, "plural": false,
{ "selections": [
"alias": null, (v1/*: any*/),
"args": null, {
"kind": "ScalarField", "alias": null,
"name": "description", "args": null,
"storageKey": null "concreteType": "DataProtectionImpactAssessment",
}, "kind": "LinkedField",
{ "name": "dataProtectionImpactAssessment",
"alias": null, "plural": false,
"args": null, "selections": [
"kind": "ScalarField", (v1/*: any*/),
"name": "necessityAndProportionality", (v2/*: any*/),
"storageKey": null (v3/*: any*/),
}, (v4/*: any*/),
{ (v5/*: any*/),
"alias": null, (v6/*: any*/),
"args": null, (v7/*: any*/),
"kind": "ScalarField", (v8/*: any*/),
"name": "potentialRisk", (v9/*: any*/),
"storageKey": null (v10/*: any*/)
}, ],
{ "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",
"storageKey": null "storageKey": null
} }
], ],
@@ -140,7 +226,7 @@ return {
"kind": "Fragment", "kind": "Fragment",
"metadata": null, "metadata": null,
"name": "ProcessingActivityGraphCreateDPIAMutation", "name": "ProcessingActivityGraphCreateDPIAMutation",
"selections": (v1/*: any*/), "selections": (v11/*: any*/),
"type": "Mutation", "type": "Mutation",
"abstractKey": null "abstractKey": null
}, },
@@ -149,19 +235,19 @@ return {
"argumentDefinitions": (v0/*: any*/), "argumentDefinitions": (v0/*: any*/),
"kind": "Operation", "kind": "Operation",
"name": "ProcessingActivityGraphCreateDPIAMutation", "name": "ProcessingActivityGraphCreateDPIAMutation",
"selections": (v1/*: any*/) "selections": (v11/*: any*/)
}, },
"params": { "params": {
"cacheID": "71220501284ee2a9761f87d8909fcd00", "cacheID": "ea459920c052d1188c193c0a7f7bf4bc",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "ProcessingActivityGraphCreateDPIAMutation", "name": "ProcessingActivityGraphCreateDPIAMutation",
"operationKind": "mutation", "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; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<540f500784ebd0386ced3eb31a6afa49>> * @generated SignedSource<<dc29d874e261d9f647f7988a70588d70>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -23,11 +23,28 @@ export type ProcessingActivityGraphCreateTIAMutation$variables = {
export type ProcessingActivityGraphCreateTIAMutation$data = { export type ProcessingActivityGraphCreateTIAMutation$data = {
readonly createTransferImpactAssessment: { readonly createTransferImpactAssessment: {
readonly transferImpactAssessment: { readonly transferImpactAssessment: {
readonly canDelete: boolean;
readonly canUpdate: boolean;
readonly createdAt: string; readonly createdAt: string;
readonly dataSubjects: string | null | undefined; readonly dataSubjects: string | null | undefined;
readonly id: string; readonly id: string;
readonly legalMechanism: string | null | undefined; readonly legalMechanism: string | null | undefined;
readonly localLawRisk: 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 supplementaryMeasures: string | null | undefined;
readonly transfer: string | null | undefined; readonly transfer: string | null | undefined;
readonly updatedAt: string; readonly updatedAt: string;
@@ -47,7 +64,89 @@ var v0 = [
"name": "input" "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, "alias": null,
"args": [ "args": [
@@ -70,60 +169,47 @@ v1 = [
"name": "transferImpactAssessment", "name": "transferImpactAssessment",
"plural": false, "plural": false,
"selections": [ "selections": [
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "concreteType": "ProcessingActivity",
"name": "id", "kind": "LinkedField",
"storageKey": null "name": "processingActivity",
}, "plural": false,
{ "selections": [
"alias": null, (v1/*: any*/),
"args": null, {
"kind": "ScalarField", "alias": null,
"name": "dataSubjects", "args": null,
"storageKey": null "concreteType": "TransferImpactAssessment",
}, "kind": "LinkedField",
{ "name": "transferImpactAssessment",
"alias": null, "plural": false,
"args": null, "selections": [
"kind": "ScalarField", (v1/*: any*/),
"name": "legalMechanism", (v2/*: any*/),
"storageKey": null (v3/*: any*/),
}, (v4/*: any*/),
{ (v5/*: any*/),
"alias": null, (v6/*: any*/),
"args": null, (v7/*: any*/),
"kind": "ScalarField", (v8/*: any*/),
"name": "transfer", (v9/*: any*/),
"storageKey": null (v10/*: any*/)
}, ],
{ "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",
"storageKey": null "storageKey": null
} }
], ],
@@ -139,7 +225,7 @@ return {
"kind": "Fragment", "kind": "Fragment",
"metadata": null, "metadata": null,
"name": "ProcessingActivityGraphCreateTIAMutation", "name": "ProcessingActivityGraphCreateTIAMutation",
"selections": (v1/*: any*/), "selections": (v11/*: any*/),
"type": "Mutation", "type": "Mutation",
"abstractKey": null "abstractKey": null
}, },
@@ -148,19 +234,19 @@ return {
"argumentDefinitions": (v0/*: any*/), "argumentDefinitions": (v0/*: any*/),
"kind": "Operation", "kind": "Operation",
"name": "ProcessingActivityGraphCreateTIAMutation", "name": "ProcessingActivityGraphCreateTIAMutation",
"selections": (v1/*: any*/) "selections": (v11/*: any*/)
}, },
"params": { "params": {
"cacheID": "70e30255cf15b77fd95a57d6ddd50c97", "cacheID": "f8ebabea46a0088051fd6f379db8b729",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "ProcessingActivityGraphCreateTIAMutation", "name": "ProcessingActivityGraphCreateTIAMutation",
"operationKind": "mutation", "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; export default node;

View File

@@ -389,6 +389,31 @@ export const createDataProtectionImpactAssessmentMutation = graphql`
residualRisk residualRisk
createdAt createdAt
updatedAt 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 supplementaryMeasures
createdAt createdAt
updatedAt 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"
)
}
}
} }
} }
} }

View File

@@ -338,6 +338,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|| undefined, || undefined,
}); });
setDpiaDeleted(false); setDpiaDeleted(false);
setShowDpiaForm(true);
toast({ toast({
title: __("Success"), title: __("Success"),
description: __("DPIA created successfully"), description: __("DPIA created successfully"),
@@ -388,6 +389,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
supplementaryMeasures: formData.supplementaryMeasures || undefined, supplementaryMeasures: formData.supplementaryMeasures || undefined,
}); });
setTiaDeleted(false); setTiaDeleted(false);
setShowTiaForm(true);
toast({ toast({
title: __("Success"), title: __("Success"),
description: __("TIA created successfully"), description: __("TIA created successfully"),

View File

@@ -22,6 +22,7 @@ import (
"time" "time"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"go.gearno.de/kit/pg" "go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
@@ -361,6 +362,13 @@ INSERT INTO processing_activity_data_protection_impact_assessments (
_, err := conn.Exec(ctx, q, args) _, err := conn.Exec(ctx, q, args)
if err != nil { 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) return fmt.Errorf("cannot insert data protection impact assessment: %w", err)
} }

View File

@@ -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, ''));

View File

@@ -22,6 +22,7 @@ import (
"time" "time"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"go.gearno.de/kit/pg" "go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
@@ -359,6 +360,13 @@ INSERT INTO processing_activity_transfer_impact_assessments (
_, err := conn.Exec(ctx, q, args) _, err := conn.Exec(ctx, q, args)
if err != nil { 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) return fmt.Errorf("cannot insert transfer impact assessment: %w", err)
} }

View File

@@ -5141,7 +5141,12 @@ func (r *mutationResolver) CreateDataProtectionImpactAssessment(ctx context.Cont
dpia, err := prb.DataProtectionImpactAssessments.Create(ctx, &req) dpia, err := prb.DataProtectionImpactAssessments.Create(ctx, &req)
if err != nil { 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{ return &types.CreateDataProtectionImpactAssessmentPayload{
@@ -5213,7 +5218,12 @@ func (r *mutationResolver) CreateTransferImpactAssessment(ctx context.Context, i
tia, err := prb.TransferImpactAssessments.Create(ctx, &req) tia, err := prb.TransferImpactAssessments.Create(ctx, &req)
if err != nil { 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{ return &types.CreateTransferImpactAssessmentPayload{