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:
@@ -94,7 +94,6 @@ const (
|
||||
|
||||
// ThirdPartyRelation actions
|
||||
ActionThirdPartyRelationCreate = "core:thirdParty-relation:create"
|
||||
ActionThirdPartyRelationDelete = "core:thirdParty-relation:delete"
|
||||
ActionThirdPartyRelationList = "core:thirdParty-relation:list"
|
||||
|
||||
// ThirdPartyContact actions
|
||||
|
||||
@@ -2357,8 +2357,16 @@ func (s *GeneratedDocumentService) buildThirdPartyListDocumentData(
|
||||
conn pg.Querier,
|
||||
organization *coredata.Organization,
|
||||
) (docgen.ThirdPartyListData, error) {
|
||||
firstLevel := 1
|
||||
|
||||
var thirdParties coredata.ThirdParties
|
||||
if err := thirdParties.LoadAllByOrganizationID(ctx, conn, scope, organization.ID); err != nil {
|
||||
if err := thirdParties.LoadAllByOrganizationID(
|
||||
ctx,
|
||||
conn,
|
||||
scope,
|
||||
organization.ID,
|
||||
coredata.NewThirdPartyFilter(nil, &firstLevel, nil),
|
||||
); err != nil {
|
||||
return docgen.ThirdPartyListData{}, fmt.Errorf("cannot load thirdParties: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ type (
|
||||
StatusPageURL *string
|
||||
BusinessOwnerID *gid.GID
|
||||
SecurityOwnerID *gid.GID
|
||||
FirstLevel *bool
|
||||
ParentThirdPartyID *gid.GID
|
||||
}
|
||||
|
||||
UpdateThirdPartyRequest struct {
|
||||
@@ -79,7 +79,6 @@ type (
|
||||
BusinessOwnerID **gid.GID
|
||||
SecurityOwnerID **gid.GID
|
||||
ShowOnTrustCenter *bool
|
||||
FirstLevel *bool
|
||||
}
|
||||
|
||||
CreateThirdPartyRiskAssessmentRequest struct {
|
||||
@@ -398,10 +397,6 @@ func (s ThirdPartyService) Update(
|
||||
thirdParty.ShowOnTrustCenter = *req.ShowOnTrustCenter
|
||||
}
|
||||
|
||||
if req.FirstLevel != nil {
|
||||
thirdParty.FirstLevel = *req.FirstLevel
|
||||
}
|
||||
|
||||
if req.TrustPageURL != nil {
|
||||
thirdParty.TrustPageURL = *req.TrustPageURL
|
||||
}
|
||||
@@ -589,11 +584,7 @@ func (s ThirdPartyService) Create(
|
||||
StatusPageURL: req.StatusPageURL,
|
||||
TermsOfServiceURL: req.TermsOfServiceURL,
|
||||
ShowOnTrustCenter: false,
|
||||
FirstLevel: true,
|
||||
}
|
||||
|
||||
if req.FirstLevel != nil {
|
||||
thirdParty.FirstLevel = *req.FirstLevel
|
||||
Level: 1,
|
||||
}
|
||||
|
||||
err := s.svc.pg.WithTx(
|
||||
@@ -606,6 +597,29 @@ func (s ThirdPartyService) Create(
|
||||
|
||||
thirdParty.OrganizationID = organization.ID
|
||||
|
||||
if req.ParentThirdPartyID != nil {
|
||||
parent := &coredata.ThirdParty{}
|
||||
if err := parent.LoadByID(ctx, conn, scope, *req.ParentThirdPartyID); err != nil {
|
||||
return fmt.Errorf("cannot load parent third party: %w", err)
|
||||
}
|
||||
|
||||
if parent.OrganizationID != organization.ID {
|
||||
return fmt.Errorf("parent third party belongs to a different organization: %w", coredata.ErrResourceNotFound)
|
||||
}
|
||||
|
||||
thirdParty.ParentThirdPartyID = &parent.ID
|
||||
// The level always follows the parent chain; ignore any
|
||||
// client-supplied level so it cannot desync from the hierarchy.
|
||||
thirdParty.Level = parent.Level + 1
|
||||
}
|
||||
|
||||
levelValidator := validator.New()
|
||||
levelValidator.Check(thirdParty.Level, "level", validator.Max(coredata.MaxThirdPartyLevel))
|
||||
|
||||
if err := levelValidator.Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.BusinessOwnerID != nil {
|
||||
businessOwner := &coredata.MembershipProfile{}
|
||||
if err := businessOwner.LoadByID(ctx, conn, scope, *req.BusinessOwnerID); err != nil {
|
||||
@@ -848,69 +862,24 @@ func (s ThirdPartyService) GetByRiskAssessmentID(
|
||||
return thirdParty, nil
|
||||
}
|
||||
|
||||
func (s ThirdPartyService) CreateThirdPartyMapping(
|
||||
func (s ThirdPartyService) GetAncestors(
|
||||
ctx context.Context,
|
||||
scope coredata.Scoper,
|
||||
parentThirdPartyID gid.GID,
|
||||
childThirdPartyID gid.GID,
|
||||
) (*coredata.ThirdParty, error) {
|
||||
childThirdParty := &coredata.ThirdParty{}
|
||||
thirdPartyID gid.GID,
|
||||
) (coredata.ThirdParties, error) {
|
||||
var ancestors coredata.ThirdParties
|
||||
|
||||
err := s.svc.pg.WithTx(
|
||||
err := s.svc.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Tx) error {
|
||||
parentThirdParty := &coredata.ThirdParty{}
|
||||
if err := parentThirdParty.LoadByID(ctx, conn, scope, parentThirdPartyID); err != nil {
|
||||
return fmt.Errorf("cannot load parent third party: %w", err)
|
||||
}
|
||||
|
||||
if err := childThirdParty.LoadByID(ctx, conn, scope, childThirdPartyID); err != nil {
|
||||
return fmt.Errorf("cannot load child third party: %w", err)
|
||||
}
|
||||
|
||||
if parentThirdParty.OrganizationID != childThirdParty.OrganizationID {
|
||||
return fmt.Errorf("cannot create mapping for third parties from different organizations: %w", coredata.ErrResourceNotFound)
|
||||
}
|
||||
|
||||
relation := &coredata.ThirdPartyThirdParty{
|
||||
ParentThirdPartyID: parentThirdPartyID,
|
||||
ChildThirdPartyID: childThirdPartyID,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := relation.Insert(ctx, conn, scope); err != nil {
|
||||
return fmt.Errorf("cannot create third party mapping: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
return ancestors.LoadAllAncestorsByThirdPartyID(ctx, conn, scope, thirdPartyID)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return childThirdParty, nil
|
||||
}
|
||||
|
||||
func (s ThirdPartyService) DeleteThirdPartyMapping(
|
||||
ctx context.Context,
|
||||
scope coredata.Scoper,
|
||||
parentThirdPartyID gid.GID,
|
||||
childThirdPartyID gid.GID,
|
||||
) error {
|
||||
return s.svc.pg.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Tx) error {
|
||||
relation := &coredata.ThirdPartyThirdParty{
|
||||
ParentThirdPartyID: parentThirdPartyID,
|
||||
ChildThirdPartyID: childThirdPartyID,
|
||||
}
|
||||
if err := relation.Delete(ctx, conn, scope); err != nil {
|
||||
return fmt.Errorf("cannot delete third party mapping: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
return ancestors, nil
|
||||
}
|
||||
|
||||
func (s ThirdPartyService) CountForParentThirdPartyID(
|
||||
|
||||
Reference in New Issue
Block a user