Add risk publish to document system

Replace the old snapshot-based system for risks with the publish
document system, mirroring the prior vendor / processing activity / DPIA
/ TIA migration. Includes the GraphQL mutation, MCP tool, CLI command,
n8n operation, frontend publish dialog, e2e tests, and a prosemirror
register template covering name, description, category, treatment,
owner, inherent and residual scoring, and notes.

The risk register lives as a generated DocumentTypeRegister document on
the organization, reused across publishes (the major version bumps on
every republish). Approvers can be passed in to create a draft pending
approval; otherwise the version is published immediately. The frontend
Risks page exposes a Publish button and a Document link button when the
document exists, and pre-fills the previous default approvers.

Risks was the last remaining snapshot type, so this commit also removes
the entire snapshot system: drop snapshotId from the Risk GraphQL type
and RiskFilter; remove RiskSnapshotter, Risks.Snapshot,
InsertRiskSnapshots, and the SnapshotID/SourceID fields on Risk; delete
Snapshot, ControlSnapshot, SnapshotsType, SnapshotOrderField,
Snapshottable, the SnapshotService, the Snapshot console resolvers and
GraphQL schema, the Snapshot MCP types and operations
(list/get/take/listControlSnapshots), the snapshot CLI (prb snapshot),
the snapshot frontend pages, routes, banner, LinkedSnapshotsCard,
SnapshotGraph, snapshot helpers, and the snapshot n8n resource and
control link/unlink snapshot operations. The snapshot_id columns remain
in the database but are now filtered out with snapshot_id IS NULL.

Add Get/Upsert/Clear GeneratedDocumentID methods on Risk backed by a new
risks_document_id column on generated_documents, matching the
ProcessingActivity/Finding/Vendor pattern. The migration command
migrate-risk-snapshots-to-documents uses raw SQL queries instead of the
Go snapshot types, since those are gone.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-04-29 18:11:19 +02:00
parent 01bc3ac696
commit 553901e4ad
93 changed files with 2384 additions and 5741 deletions

View File

@@ -140,12 +140,6 @@ export const frameworkControlNodeQuery = graphql`
canDeleteAuditMapping: permission(
action: "core:control:delete-audit-mapping"
)
canCreateSnapshotMapping: permission(
action: "core:control:create-snapshot-mapping"
)
canDeleteSnapshotMapping: permission(
action: "core:control:delete-snapshot-mapping"
)
canCreateObligationMapping: permission(
action: "core:control:create-obligation-mapping"
)
@@ -192,16 +186,6 @@ export const frameworkControlNodeQuery = graphql`
}
}
}
snapshots(first: 100)
@connection(key: "FrameworkGraphControl_snapshots") {
__id
edges {
node {
id
...LinkedSnapshotsCardFragment
}
}
}
}
}
}

View File

@@ -50,10 +50,10 @@ export function useDeleteRiskMutation() {
}
export const risksQuery = graphql`
query RiskGraphListQuery($organizationId: ID!, $snapshotId: ID) {
query RiskGraphListQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
id
...RiskGraphFragment @arguments(snapshotId: $snapshotId)
...RiskGraphFragment
}
}
`;
@@ -70,22 +70,28 @@ const risksFragment = graphql`
after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null }
snapshotId: { type: "ID", defaultValue: null }
) {
canCreateRisk: permission(action: "core:risk:create")
canPublishRisk: permission(action: "core:risk:publish")
risksDocument {
id
currentPublishedMajor
currentPublishedMinor
defaultApprovers {
id
}
}
risks(
first: $first
after: $after
last: $last
before: $before
orderBy: $order
filter: { snapshotId: $snapshotId }
) @connection(key: "RisksListQuery_risks", filters: ["filter"]) {
) @connection(key: "RisksListQuery_risks", filters: []) {
__id
edges {
node {
id
snapshotId
name
category
treatment
@@ -130,7 +136,6 @@ export const riskNodeQuery = graphql`
node(id: $riskId) {
... on Risk {
id
snapshotId
name
description
treatment

View File

@@ -1,148 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { promisifyMutation, sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useConfirm } from "@probo/ui";
import { useMutation } from "react-relay";
import { graphql } from "relay-runtime";
import { useMutationWithToasts } from "../useMutationWithToasts";
/* eslint-disable relay/unused-fields, relay/must-colocate-fragment-spreads */
export const SnapshotsConnectionKey = "SnapshotsPage_snapshots";
export const snapshotsQuery = graphql`
query SnapshotGraphListQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
... on Organization {
canCreateSnapshot: permission(action: "core:snapshot:create")
...SnapshotsPageFragment
}
}
}
`;
export const snapshotNodeQuery = graphql`
query SnapshotGraphNodeQuery($snapshotId: ID!) {
node(id: $snapshotId) {
... on Snapshot {
id
name
description
type
organization {
id
name
}
createdAt
}
}
}
`;
export const createSnapshotMutation = graphql`
mutation SnapshotGraphCreateMutation(
$input: CreateSnapshotInput!
$connections: [ID!]!
) {
createSnapshot(input: $input) {
snapshotEdge @prependEdge(connections: $connections) {
node {
id
name
description
type
createdAt
}
}
}
}
`;
export const deleteSnapshotMutation = graphql`
mutation SnapshotGraphDeleteMutation(
$input: DeleteSnapshotInput!
$connections: [ID!]!
) {
deleteSnapshot(input: $input) {
deletedSnapshotId @deleteEdge(connections: $connections)
}
}
`;
export const useDeleteSnapshot = (
snapshot: { id: string; name: string },
connectionId: string,
) => {
const { __ } = useTranslate();
const [mutate] = useMutationWithToasts(deleteSnapshotMutation, {
successMessage: __("Snapshot deleted successfully"),
errorMessage: __("Failed to delete snapshot"),
});
const confirm = useConfirm();
return () => {
confirm(
() =>
mutate({
variables: {
input: {
snapshotId: snapshot.id,
},
connections: [connectionId],
},
}),
{
message: sprintf(
__(
"This will permanently delete the snapshot %s. This action cannot be undone.",
),
snapshot.name,
),
},
);
};
};
export const useCreateSnapshot = (connectionId: string) => {
// eslint-disable-next-line relay/generated-typescript-types
const [mutate] = useMutation(createSnapshotMutation);
const { __ } = useTranslate();
return (input: {
organizationId: string;
name: string;
description?: string;
}) => {
if (!input.organizationId) {
return alert(__("Failed to create snapshot: organization is required"));
}
if (!input.name) {
return alert(__("Failed to create snapshot: name is required"));
}
return promisifyMutation(mutate)({
variables: {
input: {
organizationId: input.organizationId,
name: input.name,
description: input.description,
},
connections: [connectionId],
},
});
};
};