Scope sub-third-parties per parent

Replace the many-to-many junction table with a direct
parent_third_party_id foreign key on third_parties. Each
sub-third-party now belongs to exactly one parent, making
duplicates across parents independent entities.

Replace the firstLevel boolean with an integer level field
(1 = direct, 2+ = parent level + 1) to support arbitrary
nesting depth.

Remove the createThirdPartyThirdPartyMapping and
deleteThirdPartyThirdPartyMapping mutations, the CLI
link/unlink commands, and the corresponding MCP tools.
Creating a child third party now just requires passing
parentThirdPartyId on the existing createThirdParty mutation.

The frontend walks the parentThirdParty chain to build
display names like "Name (Ancestor1/Ancestor2)" and shows
clickable ancestor links on the detail page.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-05-29 16:22:54 +02:00
parent ec858e58df
commit b6781d3de0
36 changed files with 1276 additions and 1192 deletions

View File

@@ -22,8 +22,7 @@ import { graphql } from "relay-runtime";
import type { ThirdPartyGraphCreateMutation } from "#/__generated__/core/ThirdPartyGraphCreateMutation.graphql";
import type { ThirdPartyGraphDeleteMutation } from "#/__generated__/core/ThirdPartyGraphDeleteMutation.graphql";
import type { ThirdPartyGraphSelectQuery } from "#/__generated__/core/ThirdPartyGraphSelectQuery.graphql";
import { useMutationWithToasts } from "../useMutationWithToasts";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
/* eslint-disable relay/unused-fields, relay/must-colocate-fragment-spreads */
@@ -51,14 +50,10 @@ const createThirdPartyMutation = graphql`
export function useCreateThirdPartyMutation() {
const { __ } = useTranslate();
return useMutationWithToasts<ThirdPartyGraphCreateMutation>(
createThirdPartyMutation,
{
successMessage: __("Third party created successfully."),
errorMessage: __("Failed to create third party"),
},
);
return useMutationWithToasts<ThirdPartyGraphCreateMutation>(createThirdPartyMutation, {
successMessage: __("Third party created successfully"),
errorMessage: __("Failed to create third party"),
});
}
const deleteThirdPartyMutation = graphql`
@@ -138,7 +133,7 @@ export const paginatedThirdPartiesFragment = graphql`
after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null }
filter: { type: "ThirdPartyFilter", defaultValue: { firstLevel: true } }
filter: { type: "ThirdPartyFilter", defaultValue: { level: 1 } }
) {
thirdParties(
first: $first
@@ -154,6 +149,7 @@ export const paginatedThirdPartiesFragment = graphql`
id
name
websiteUrl
level
updatedAt
riskAssessments(
first: 1
@@ -184,7 +180,11 @@ export const thirdPartyNodeQuery = graphql`
... on ThirdParty {
name
websiteUrl
firstLevel
level
ancestors {
id
name
}
vettingStatus
canVet: permission(action: "core:thirdParty:vet")
canUpdate: permission(action: "core:thirdParty:update")
@@ -229,14 +229,13 @@ export const thirdPartiesSelectQuery = graphql`
thirdParties(
first: 100
orderBy: { direction: ASC, field: NAME }
filter: { firstLevel: true }
) {
edges {
node {
id
name
websiteUrl
firstLevel
level
}
}
}