Add third-party self-referential relations
Introduce a self-referential many-to-many relation table so a third party can have child third parties. Each relation is directional (parent to child); both directions can coexist as independent rows. Add a first_level boolean on third_parties (default true) with a filter on the list page that defaults to showing only first-level third parties. Frontend adds a "Third Parties" tab on the detail page where users can link existing third parties or create new ones from the common third party catalog (created as non-first-level). The list page gets a First Level/All toggle filter. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
108
pkg/cmd/thirdpartymgmt/link/link.go
Normal file
108
pkg/cmd/thirdpartymgmt/link/link.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// 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.
|
||||
|
||||
package link
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.probo.inc/probo/pkg/cli/api"
|
||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||
)
|
||||
|
||||
const linkMutation = `
|
||||
mutation($input: CreateThirdPartyThirdPartyMappingInput!) {
|
||||
createThirdPartyThirdPartyMapping(input: $input) {
|
||||
thirdPartyEdge {
|
||||
node {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
type linkResponse struct {
|
||||
CreateThirdPartyThirdPartyMapping struct {
|
||||
ThirdPartyEdge struct {
|
||||
Node struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"node"`
|
||||
} `json:"thirdPartyEdge"`
|
||||
} `json:"createThirdPartyThirdPartyMapping"`
|
||||
}
|
||||
|
||||
func NewCmdLink(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "link <parent-id> <child-id>",
|
||||
Short: "Link a child thirdParty to a parent thirdParty",
|
||||
Example: ` # Link a child third_party to a parent
|
||||
prb thirdParty link <parent-id> <child-id>`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
host, hc, err := cfg.DefaultHost()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := api.NewClient(
|
||||
host,
|
||||
hc.Token,
|
||||
"/api/console/v1/graphql",
|
||||
cfg.HTTPTimeoutDuration(),
|
||||
cmdutil.TokenRefreshOption(cfg, host, hc),
|
||||
)
|
||||
|
||||
data, err := client.Do(
|
||||
linkMutation,
|
||||
map[string]any{
|
||||
"input": map[string]any{
|
||||
"parentThirdPartyId": args[0],
|
||||
"childThirdPartyId": args[1],
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var resp linkResponse
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return fmt.Errorf("cannot parse response: %w", err)
|
||||
}
|
||||
|
||||
v := resp.CreateThirdPartyThirdPartyMapping.ThirdPartyEdge.Node
|
||||
_, _ = fmt.Fprintf(
|
||||
f.IOStreams.Out,
|
||||
"Linked thirdParty %s (%s) as child of %s\n",
|
||||
v.ID,
|
||||
v.Name,
|
||||
args[0],
|
||||
)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -24,11 +24,11 @@ import (
|
||||
)
|
||||
|
||||
const listQuery = `
|
||||
query($id: ID!, $first: Int, $after: CursorKey, $orderBy: ThirdPartyOrder) {
|
||||
query($id: ID!, $first: Int, $after: CursorKey, $orderBy: ThirdPartyOrder, $filter: ThirdPartyFilter) {
|
||||
node(id: $id) {
|
||||
__typename
|
||||
... on Organization {
|
||||
third_parties(first: $first, after: $after, orderBy: $orderBy) {
|
||||
third_parties(first: $first, after: $after, orderBy: $orderBy, filter: $filter) {
|
||||
totalCount
|
||||
edges {
|
||||
node {
|
||||
@@ -55,11 +55,12 @@ type thirdParty struct {
|
||||
|
||||
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
|
||||
var (
|
||||
flagOrg string
|
||||
flagLimit int
|
||||
flagOrderBy string
|
||||
flagOrderDir string
|
||||
flagOutput *string
|
||||
flagOrg string
|
||||
flagLimit int
|
||||
flagOrderBy string
|
||||
flagOrderDir string
|
||||
flagFirstLevel bool
|
||||
flagOutput *string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -107,6 +108,12 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
|
||||
"id": flagOrg,
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("first-level") {
|
||||
variables["filter"] = map[string]any{
|
||||
"first-level": flagFirstLevel,
|
||||
}
|
||||
}
|
||||
|
||||
if flagOrderBy != "" {
|
||||
if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"NAME", "CREATED_AT", "UPDATED_AT"}); err != nil {
|
||||
return err
|
||||
@@ -188,6 +195,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of thirdParties to list")
|
||||
cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (NAME, CREATED_AT, UPDATED_AT)")
|
||||
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
|
||||
cmd.Flags().BoolVar(&flagFirstLevel, "first-level", false, "Filter by first-level thirdParties only")
|
||||
flagOutput = cmdutil.AddOutputFlag(cmd)
|
||||
|
||||
return cmd
|
||||
|
||||
@@ -20,8 +20,10 @@ import (
|
||||
"go.probo.inc/probo/pkg/cmd/thirdpartymgmt/assess"
|
||||
"go.probo.inc/probo/pkg/cmd/thirdpartymgmt/create"
|
||||
"go.probo.inc/probo/pkg/cmd/thirdpartymgmt/delete"
|
||||
"go.probo.inc/probo/pkg/cmd/thirdpartymgmt/link"
|
||||
"go.probo.inc/probo/pkg/cmd/thirdpartymgmt/list"
|
||||
"go.probo.inc/probo/pkg/cmd/thirdpartymgmt/publish"
|
||||
"go.probo.inc/probo/pkg/cmd/thirdpartymgmt/unlink"
|
||||
"go.probo.inc/probo/pkg/cmd/thirdpartymgmt/update"
|
||||
"go.probo.inc/probo/pkg/cmd/thirdpartymgmt/view"
|
||||
)
|
||||
@@ -39,6 +41,8 @@ func NewCmdThirdParty(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd.AddCommand(delete.NewCmdDelete(f))
|
||||
cmd.AddCommand(assess.NewCmdAssess(f))
|
||||
cmd.AddCommand(publish.NewCmdPublish(f))
|
||||
cmd.AddCommand(link.NewCmdLink(f))
|
||||
cmd.AddCommand(unlink.NewCmdUnlink(f))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
84
pkg/cmd/thirdpartymgmt/unlink/unlink.go
Normal file
84
pkg/cmd/thirdpartymgmt/unlink/unlink.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// 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.
|
||||
|
||||
package unlink
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.probo.inc/probo/pkg/cli/api"
|
||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||
)
|
||||
|
||||
const unlinkMutation = `
|
||||
mutation($input: UncreateThirdPartyThirdPartyMappingInput!) {
|
||||
uncreateThirdPartyThirdPartyMapping(input: $input) {
|
||||
removedThirdPartyId
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func NewCmdUnlink(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "unlink <parent-id> <child-id>",
|
||||
Short: "Unlink a child thirdParty from a parent thirdParty",
|
||||
Example: ` # Unlink a child third_party from a parent
|
||||
prb thirdParty unlink <parent-id> <child-id>`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
host, hc, err := cfg.DefaultHost()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := api.NewClient(
|
||||
host,
|
||||
hc.Token,
|
||||
"/api/console/v1/graphql",
|
||||
cfg.HTTPTimeoutDuration(),
|
||||
cmdutil.TokenRefreshOption(cfg, host, hc),
|
||||
)
|
||||
|
||||
_, err = client.Do(
|
||||
unlinkMutation,
|
||||
map[string]any{
|
||||
"input": map[string]any{
|
||||
"parentThirdPartyId": args[0],
|
||||
"childThirdPartyId": args[1],
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(
|
||||
f.IOStreams.Out,
|
||||
"Unlinked thirdParty %s from parent %s\n",
|
||||
args[1],
|
||||
args[0],
|
||||
)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
25
pkg/coredata/migrations/20260519T100000Z.sql
Normal file
25
pkg/coredata/migrations/20260519T100000Z.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
-- 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.
|
||||
|
||||
ALTER TABLE third_parties ADD COLUMN first_level boolean NOT NULL DEFAULT true;
|
||||
ALTER TABLE third_parties ALTER COLUMN first_level DROP DEFAULT;
|
||||
|
||||
CREATE TABLE third_party_third_parties (
|
||||
parent_third_party_id text NOT NULL REFERENCES third_parties(id) ON DELETE CASCADE,
|
||||
child_third_party_id text NOT NULL REFERENCES third_parties(id) ON DELETE CASCADE,
|
||||
tenant_id bytea NOT NULL,
|
||||
created_at timestamptz NOT NULL,
|
||||
PRIMARY KEY (parent_third_party_id, child_third_party_id),
|
||||
CHECK (parent_third_party_id <> child_third_party_id)
|
||||
);
|
||||
@@ -162,6 +162,7 @@ type (
|
||||
SecurityPageURL *string `db:"security_page_url"`
|
||||
TrustPageURL *string `db:"trust_page_url"`
|
||||
ShowOnTrustCenter bool `db:"show_on_trust_center"`
|
||||
FirstLevel bool `db:"first_level"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
@@ -253,6 +254,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -320,6 +322,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -381,6 +384,7 @@ INSERT INTO
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
@@ -409,6 +413,7 @@ VALUES (
|
||||
@security_page_url,
|
||||
@trust_page_url,
|
||||
@show_on_trust_center,
|
||||
@first_level,
|
||||
@created_at,
|
||||
@updated_at
|
||||
)
|
||||
@@ -439,6 +444,7 @@ VALUES (
|
||||
"security_page_url": v.SecurityPageURL,
|
||||
"trust_page_url": v.TrustPageURL,
|
||||
"show_on_trust_center": v.ShowOnTrustCenter,
|
||||
"first_level": v.FirstLevel,
|
||||
"created_at": v.CreatedAt,
|
||||
"updated_at": v.UpdatedAt,
|
||||
}
|
||||
@@ -534,6 +540,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -597,6 +604,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -657,6 +665,7 @@ SET
|
||||
business_owner_profile_id = @business_owner_profile_id,
|
||||
security_owner_profile_id = @security_owner_profile_id,
|
||||
show_on_trust_center = @show_on_trust_center,
|
||||
first_level = @first_level,
|
||||
updated_at = @updated_at
|
||||
WHERE %s
|
||||
AND id = @third_party_id
|
||||
@@ -686,6 +695,7 @@ WHERE %s
|
||||
"business_owner_profile_id": v.BusinessOwnerID,
|
||||
"security_owner_profile_id": v.SecurityOwnerID,
|
||||
"show_on_trust_center": v.ShowOnTrustCenter,
|
||||
"first_level": v.FirstLevel,
|
||||
}
|
||||
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
@@ -803,6 +813,7 @@ WITH vend AS (
|
||||
v.security_page_url,
|
||||
v.trust_page_url,
|
||||
v.show_on_trust_center,
|
||||
v.first_level,
|
||||
v.created_at,
|
||||
v.updated_at
|
||||
FROM
|
||||
@@ -837,6 +848,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -938,6 +950,7 @@ WITH vend AS (
|
||||
v.security_page_url,
|
||||
v.trust_page_url,
|
||||
v.show_on_trust_center,
|
||||
v.first_level,
|
||||
v.created_at,
|
||||
v.updated_at
|
||||
FROM
|
||||
@@ -972,6 +985,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -1033,6 +1047,7 @@ WITH vend AS (
|
||||
v.security_page_url,
|
||||
v.trust_page_url,
|
||||
v.show_on_trust_center,
|
||||
v.first_level,
|
||||
v.created_at,
|
||||
v.updated_at
|
||||
FROM
|
||||
@@ -1067,6 +1082,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -1129,6 +1145,7 @@ WITH vend AS (
|
||||
v.security_page_url,
|
||||
v.trust_page_url,
|
||||
v.show_on_trust_center,
|
||||
v.first_level,
|
||||
v.created_at,
|
||||
v.updated_at
|
||||
FROM
|
||||
@@ -1163,6 +1180,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -1293,6 +1311,7 @@ WITH vend AS (
|
||||
v.security_page_url,
|
||||
v.trust_page_url,
|
||||
v.show_on_trust_center,
|
||||
v.first_level,
|
||||
v.created_at,
|
||||
v.updated_at
|
||||
FROM
|
||||
@@ -1327,6 +1346,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -1387,6 +1407,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
|
||||
@@ -21,12 +21,14 @@ import (
|
||||
type (
|
||||
ThirdPartyFilter struct {
|
||||
showOnTrustCenter *bool
|
||||
firstLevel *bool
|
||||
}
|
||||
)
|
||||
|
||||
func NewThirdPartyFilter(showOnTrustCenter *bool) *ThirdPartyFilter {
|
||||
func NewThirdPartyFilter(showOnTrustCenter *bool, firstLevel *bool) *ThirdPartyFilter {
|
||||
return &ThirdPartyFilter{
|
||||
showOnTrustCenter: showOnTrustCenter,
|
||||
firstLevel: firstLevel,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +41,12 @@ func (f *ThirdPartyFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||
args["show_on_trust_center"] = nil
|
||||
}
|
||||
|
||||
if f.firstLevel != nil {
|
||||
args["first_level"] = *f.firstLevel
|
||||
} else {
|
||||
args["first_level"] = nil
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
@@ -50,5 +58,13 @@ func (f *ThirdPartyFilter) SQLFragment() string {
|
||||
show_on_trust_center = @show_on_trust_center::boolean
|
||||
ELSE TRUE
|
||||
END
|
||||
)
|
||||
AND
|
||||
(
|
||||
CASE
|
||||
WHEN @first_level::boolean IS NOT NULL THEN
|
||||
first_level = @first_level::boolean
|
||||
ELSE TRUE
|
||||
END
|
||||
)`
|
||||
}
|
||||
|
||||
226
pkg/coredata/third_party_third_party.go
Normal file
226
pkg/coredata/third_party_third_party.go
Normal file
@@ -0,0 +1,226 @@
|
||||
// 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.
|
||||
|
||||
package coredata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
ThirdPartyThirdParty struct {
|
||||
ParentThirdPartyID gid.GID `db:"parent_third_party_id"`
|
||||
ChildThirdPartyID gid.GID `db:"child_third_party_id"`
|
||||
TenantID gid.TenantID `db:"tenant_id"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
|
||||
ThirdPartyThirdParties []*ThirdPartyThirdParty
|
||||
)
|
||||
|
||||
func (r *ThirdPartyThirdParty) Insert(ctx context.Context, conn pg.Tx, scope Scoper) error {
|
||||
q := `
|
||||
INSERT INTO third_party_third_parties (
|
||||
parent_third_party_id,
|
||||
child_third_party_id,
|
||||
tenant_id,
|
||||
created_at
|
||||
) VALUES (
|
||||
@parent_third_party_id,
|
||||
@child_third_party_id,
|
||||
@tenant_id,
|
||||
@created_at
|
||||
)
|
||||
ON CONFLICT (parent_third_party_id, child_third_party_id) DO NOTHING
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"parent_third_party_id": r.ParentThirdPartyID,
|
||||
"child_third_party_id": r.ChildThirdPartyID,
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"created_at": r.CreatedAt,
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert third party third party: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ThirdPartyThirdParty) Delete(ctx context.Context, conn pg.Tx, scope Scoper) error {
|
||||
q := `
|
||||
DELETE FROM third_party_third_parties
|
||||
WHERE %s
|
||||
AND parent_third_party_id = @parent_third_party_id
|
||||
AND child_third_party_id = @child_third_party_id
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"parent_third_party_id": r.ParentThirdPartyID,
|
||||
"child_third_party_id": r.ChildThirdPartyID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (v *ThirdParties) CountByParentThirdPartyID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
parentThirdPartyID gid.GID,
|
||||
) (int, error) {
|
||||
q := `
|
||||
WITH children AS (
|
||||
SELECT
|
||||
tp.id,
|
||||
tp.tenant_id
|
||||
FROM
|
||||
third_parties tp
|
||||
INNER JOIN
|
||||
third_party_third_parties tpr ON tp.id = tpr.child_third_party_id
|
||||
WHERE
|
||||
tpr.parent_third_party_id = @parent_third_party_id
|
||||
)
|
||||
SELECT
|
||||
COUNT(id)
|
||||
FROM
|
||||
children
|
||||
WHERE %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"parent_third_party_id": parentThirdPartyID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
var count int
|
||||
|
||||
err := conn.QueryRow(ctx, q, args).Scan(&count)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot count child third parties: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (v *ThirdParties) LoadByParentThirdPartyID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
parentThirdPartyID gid.GID,
|
||||
cursor *page.Cursor[ThirdPartyOrderField],
|
||||
) error {
|
||||
q := `
|
||||
WITH children AS (
|
||||
SELECT
|
||||
tp.id,
|
||||
tp.tenant_id,
|
||||
tp.organization_id,
|
||||
tp.common_third_party_id,
|
||||
tp.name,
|
||||
tp.description,
|
||||
tp.category,
|
||||
tp.headquarter_address,
|
||||
tp.legal_name,
|
||||
tp.website_url,
|
||||
tp.privacy_policy_url,
|
||||
tp.service_level_agreement_url,
|
||||
tp.data_processing_agreement_url,
|
||||
tp.business_associate_agreement_url,
|
||||
tp.subprocessors_list_url,
|
||||
tp.certifications,
|
||||
tp.countries,
|
||||
tp.business_owner_profile_id,
|
||||
tp.security_owner_profile_id,
|
||||
tp.status_page_url,
|
||||
tp.terms_of_service_url,
|
||||
tp.security_page_url,
|
||||
tp.trust_page_url,
|
||||
tp.show_on_trust_center,
|
||||
tp.first_level,
|
||||
tp.created_at,
|
||||
tp.updated_at
|
||||
FROM
|
||||
third_parties tp
|
||||
INNER JOIN
|
||||
third_party_third_parties tpr ON tp.id = tpr.child_third_party_id
|
||||
WHERE
|
||||
tpr.parent_third_party_id = @parent_third_party_id
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
tenant_id,
|
||||
organization_id,
|
||||
common_third_party_id,
|
||||
name,
|
||||
description,
|
||||
category,
|
||||
headquarter_address,
|
||||
legal_name,
|
||||
website_url,
|
||||
privacy_policy_url,
|
||||
service_level_agreement_url,
|
||||
data_processing_agreement_url,
|
||||
business_associate_agreement_url,
|
||||
subprocessors_list_url,
|
||||
certifications,
|
||||
countries,
|
||||
business_owner_profile_id,
|
||||
security_owner_profile_id,
|
||||
status_page_url,
|
||||
terms_of_service_url,
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
children
|
||||
WHERE %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"parent_third_party_id": parentThirdPartyID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query child third parties: %w", err)
|
||||
}
|
||||
|
||||
thirdParties, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ThirdParty])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect child third parties: %w", err)
|
||||
}
|
||||
|
||||
*v = thirdParties
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -92,6 +92,11 @@ const (
|
||||
ActionThirdPartyAssess = "core:thirdParty:assess"
|
||||
ActionThirdPartyPublish = "core:thirdParty:publish"
|
||||
|
||||
// ThirdPartyRelation actions
|
||||
ActionThirdPartyRelationCreate = "core:thirdParty-relation:create"
|
||||
ActionThirdPartyRelationDelete = "core:thirdParty-relation:delete"
|
||||
ActionThirdPartyRelationList = "core:thirdParty-relation:list"
|
||||
|
||||
// ThirdPartyContact actions
|
||||
ActionThirdPartyContactGet = "core:thirdParty-contact:get"
|
||||
ActionThirdPartyContactList = "core:thirdParty-contact:list"
|
||||
|
||||
@@ -55,6 +55,7 @@ var ViewerPolicy = policy.NewPolicy(
|
||||
ActionThirdPartyBusinessAssociateAgreementGet,
|
||||
ActionThirdPartyDataPrivacyAgreementGet,
|
||||
ActionThirdPartyRiskAssessmentList,
|
||||
ActionThirdPartyRelationList,
|
||||
ActionFrameworkGet, ActionFrameworkList,
|
||||
ActionControlGet, ActionControlList,
|
||||
ActionMeasureGet, ActionMeasureList,
|
||||
@@ -140,6 +141,7 @@ var AuditorPolicy = policy.NewPolicy(
|
||||
ActionThirdPartyBusinessAssociateAgreementGet,
|
||||
ActionThirdPartyDataPrivacyAgreementGet,
|
||||
ActionThirdPartyRiskAssessmentList,
|
||||
ActionThirdPartyRelationList,
|
||||
ActionFrameworkGet, ActionFrameworkList,
|
||||
ActionControlGet, ActionControlList,
|
||||
ActionMeasureGet, ActionMeasureList,
|
||||
|
||||
@@ -92,6 +92,7 @@ type (
|
||||
StatusPageURL *string
|
||||
BusinessOwnerID *gid.GID
|
||||
SecurityOwnerID *gid.GID
|
||||
FirstLevel *bool
|
||||
}
|
||||
|
||||
UpdateThirdPartyRequest struct {
|
||||
@@ -116,6 +117,7 @@ type (
|
||||
BusinessOwnerID **gid.GID
|
||||
SecurityOwnerID **gid.GID
|
||||
ShowOnTrustCenter *bool
|
||||
FirstLevel *bool
|
||||
}
|
||||
|
||||
AssessThirdPartyRequest struct {
|
||||
@@ -386,6 +388,10 @@ func (s ThirdPartyService) Update(
|
||||
thirdParty.ShowOnTrustCenter = *req.ShowOnTrustCenter
|
||||
}
|
||||
|
||||
if req.FirstLevel != nil {
|
||||
thirdParty.FirstLevel = *req.FirstLevel
|
||||
}
|
||||
|
||||
if req.TrustPageURL != nil {
|
||||
thirdParty.TrustPageURL = *req.TrustPageURL
|
||||
}
|
||||
@@ -573,6 +579,11 @@ func (s ThirdPartyService) Create(
|
||||
StatusPageURL: req.StatusPageURL,
|
||||
TermsOfServiceURL: req.TermsOfServiceURL,
|
||||
ShowOnTrustCenter: false,
|
||||
FirstLevel: true,
|
||||
}
|
||||
|
||||
if req.FirstLevel != nil {
|
||||
thirdParty.FirstLevel = *req.FirstLevel
|
||||
}
|
||||
|
||||
err := s.svc.pg.WithTx(
|
||||
@@ -947,3 +958,116 @@ func (s ThirdPartyService) Assess(
|
||||
Subprocessors: subprocessors,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s ThirdPartyService) CreateThirdPartyMapping(
|
||||
ctx context.Context,
|
||||
scope coredata.Scoper,
|
||||
parentThirdPartyID gid.GID,
|
||||
childThirdPartyID gid.GID,
|
||||
) (*coredata.ThirdParty, error) {
|
||||
childThirdParty := &coredata.ThirdParty{}
|
||||
|
||||
err := s.svc.pg.WithTx(
|
||||
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
|
||||
},
|
||||
)
|
||||
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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (s ThirdPartyService) CountForParentThirdPartyID(
|
||||
ctx context.Context,
|
||||
scope coredata.Scoper,
|
||||
parentThirdPartyID gid.GID,
|
||||
) (int, error) {
|
||||
var count int
|
||||
|
||||
err := s.svc.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) (err error) {
|
||||
thirdParties := coredata.ThirdParties{}
|
||||
|
||||
count, err = thirdParties.CountByParentThirdPartyID(ctx, conn, scope, parentThirdPartyID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count child third parties: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s ThirdPartyService) ListForParentThirdPartyID(
|
||||
ctx context.Context,
|
||||
scope coredata.Scoper,
|
||||
parentThirdPartyID gid.GID,
|
||||
cursor *page.Cursor[coredata.ThirdPartyOrderField],
|
||||
) (*page.Page[*coredata.ThirdParty, coredata.ThirdPartyOrderField], error) {
|
||||
var thirdParties coredata.ThirdParties
|
||||
|
||||
err := s.svc.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
return thirdParties.LoadByParentThirdPartyID(ctx, conn, scope, parentThirdPartyID, cursor)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return page.NewPage(thirdParties, cursor), nil
|
||||
}
|
||||
|
||||
@@ -331,6 +331,7 @@ type Organization implements Node {
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: ThirdPartyOrder
|
||||
filter: ThirdPartyFilter
|
||||
): ThirdPartyConnection! @goField(forceResolver: true)
|
||||
|
||||
thirdPartiesDocument: Document @goField(forceResolver: true)
|
||||
|
||||
@@ -177,6 +177,10 @@ input ThirdPartyOrder
|
||||
field: ThirdPartyOrderField!
|
||||
}
|
||||
|
||||
input ThirdPartyFilter {
|
||||
firstLevel: Boolean
|
||||
}
|
||||
|
||||
input ThirdPartyComplianceReportOrder
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.ThirdPartyComplianceReportOrderBy"
|
||||
@@ -269,6 +273,16 @@ type ThirdParty implements Node {
|
||||
legalName: String
|
||||
websiteUrl: String
|
||||
showOnTrustCenter: Boolean!
|
||||
firstLevel: Boolean!
|
||||
|
||||
childThirdParties(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: ThirdPartyOrder
|
||||
): ThirdPartyConnection! @goField(forceResolver: true)
|
||||
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
|
||||
@@ -460,6 +474,12 @@ extend type Mutation {
|
||||
publishThirdPartyList(
|
||||
input: PublishThirdPartyListInput!
|
||||
): PublishThirdPartyListPayload!
|
||||
createThirdPartyThirdPartyMapping(
|
||||
input: CreateThirdPartyThirdPartyMappingInput!
|
||||
): CreateThirdPartyThirdPartyMappingPayload!
|
||||
deleteThirdPartyThirdPartyMapping(
|
||||
input: DeleteThirdPartyThirdPartyMappingInput!
|
||||
): DeleteThirdPartyThirdPartyMappingPayload!
|
||||
}
|
||||
|
||||
input PublishThirdPartyListInput {
|
||||
@@ -494,6 +514,7 @@ input CreateThirdPartyInput {
|
||||
termsOfServiceUrl: String
|
||||
businessOwnerId: ID
|
||||
securityOwnerId: ID
|
||||
firstLevel: Boolean
|
||||
}
|
||||
|
||||
input UpdateThirdPartyInput {
|
||||
@@ -518,6 +539,7 @@ input UpdateThirdPartyInput {
|
||||
businessOwnerId: ID @goField(omittable: true)
|
||||
securityOwnerId: ID @goField(omittable: true)
|
||||
showOnTrustCenter: Boolean
|
||||
firstLevel: Boolean
|
||||
}
|
||||
|
||||
input DeleteThirdPartyInput {
|
||||
@@ -709,3 +731,21 @@ type AssessThirdPartyPayload {
|
||||
report: String!
|
||||
subprocessors: [ThirdPartySubprocessor!]!
|
||||
}
|
||||
|
||||
input CreateThirdPartyThirdPartyMappingInput {
|
||||
parentThirdPartyId: ID!
|
||||
childThirdPartyId: ID!
|
||||
}
|
||||
|
||||
type CreateThirdPartyThirdPartyMappingPayload {
|
||||
thirdPartyEdge: ThirdPartyEdge!
|
||||
}
|
||||
|
||||
input DeleteThirdPartyThirdPartyMappingInput {
|
||||
parentThirdPartyId: ID!
|
||||
childThirdPartyId: ID!
|
||||
}
|
||||
|
||||
type DeleteThirdPartyThirdPartyMappingPayload {
|
||||
removedThirdPartyId: ID!
|
||||
}
|
||||
|
||||
@@ -1271,7 +1271,7 @@ func (r *organizationResolver) CookieBanners(ctx context.Context, obj *types.Org
|
||||
}
|
||||
|
||||
// ThirdParties is the resolver for the thirdParties field.
|
||||
func (r *organizationResolver) ThirdParties(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ThirdPartyOrderBy) (*types.ThirdPartyConnection, error) {
|
||||
func (r *organizationResolver) ThirdParties(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ThirdPartyOrderBy, filter *types.ThirdPartyFilter) (*types.ThirdPartyConnection, error) {
|
||||
scope, err := r.authorize(ctx, obj.ID, probo.ActionThirdPartyList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1291,7 +1291,12 @@ func (r *organizationResolver) ThirdParties(ctx context.Context, obj *types.Orga
|
||||
|
||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
thirdPartyFilter := coredata.NewThirdPartyFilter(nil)
|
||||
var firstLevel *bool
|
||||
if filter != nil {
|
||||
firstLevel = filter.FirstLevel
|
||||
}
|
||||
|
||||
thirdPartyFilter := coredata.NewThirdPartyFilter(nil, firstLevel)
|
||||
|
||||
page, err := r.probo.ThirdParties.ListForOrganizationID(ctx, scope, obj.ID, cursor, thirdPartyFilter)
|
||||
if err != nil {
|
||||
|
||||
@@ -55,6 +55,7 @@ func (r *mutationResolver) CreateThirdParty(ctx context.Context, input types.Cre
|
||||
BusinessOwnerID: input.BusinessOwnerID,
|
||||
SecurityOwnerID: input.SecurityOwnerID,
|
||||
Countries: input.Countries,
|
||||
FirstLevel: input.FirstLevel,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -106,6 +107,7 @@ func (r *mutationResolver) UpdateThirdParty(ctx context.Context, input types.Upd
|
||||
BusinessOwnerID: gqlutils.UnwrapOmittable(input.BusinessOwnerID),
|
||||
SecurityOwnerID: gqlutils.UnwrapOmittable(input.SecurityOwnerID),
|
||||
ShowOnTrustCenter: input.ShowOnTrustCenter,
|
||||
FirstLevel: input.FirstLevel,
|
||||
Countries: input.Countries,
|
||||
},
|
||||
)
|
||||
@@ -594,6 +596,46 @@ func (r *mutationResolver) PublishThirdPartyList(ctx context.Context, input type
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateThirdPartyThirdPartyMapping is the resolver for the linkThirdPartyThirdParty field.
|
||||
func (r *mutationResolver) CreateThirdPartyThirdPartyMapping(ctx context.Context, input types.CreateThirdPartyThirdPartyMappingInput) (*types.CreateThirdPartyThirdPartyMappingPayload, error) {
|
||||
scope, err := r.authorize(ctx, input.ParentThirdPartyID, probo.ActionThirdPartyRelationCreate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
childThirdParty, err := r.probo.ThirdParties.CreateThirdPartyMapping(ctx, scope, input.ParentThirdPartyID, input.ChildThirdPartyID)
|
||||
if err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return nil, gqlutils.NotFound(ctx, err)
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot create third party mapping", log.Error(err))
|
||||
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return &types.CreateThirdPartyThirdPartyMappingPayload{
|
||||
ThirdPartyEdge: types.NewThirdPartyEdge(childThirdParty, coredata.ThirdPartyOrderFieldName),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteThirdPartyThirdPartyMapping is the resolver for the deleteThirdPartyThirdPartyMapping field.
|
||||
func (r *mutationResolver) DeleteThirdPartyThirdPartyMapping(ctx context.Context, input types.DeleteThirdPartyThirdPartyMappingInput) (*types.DeleteThirdPartyThirdPartyMappingPayload, error) {
|
||||
scope, err := r.authorize(ctx, input.ParentThirdPartyID, probo.ActionThirdPartyRelationDelete)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.probo.ThirdParties.DeleteThirdPartyMapping(ctx, scope, input.ParentThirdPartyID, input.ChildThirdPartyID); err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot delete third party mapping", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return &types.DeleteThirdPartyThirdPartyMappingPayload{
|
||||
RemovedThirdPartyID: input.ChildThirdPartyID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Organization is the resolver for the organization field.
|
||||
func (r *thirdPartyResolver) Organization(ctx context.Context, obj *types.ThirdParty) (*types.Organization, error) {
|
||||
if _, err := r.authorize(ctx, obj.ID, probo.ActionOrganizationGet); err != nil {
|
||||
@@ -830,6 +872,35 @@ func (r *thirdPartyResolver) SecurityOwner(ctx context.Context, obj *types.Third
|
||||
return types.NewProfile(securityOwner), nil
|
||||
}
|
||||
|
||||
// ChildThirdParties is the resolver for the childThirdParties field.
|
||||
func (r *thirdPartyResolver) ChildThirdParties(ctx context.Context, obj *types.ThirdParty, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ThirdPartyOrderBy) (*types.ThirdPartyConnection, error) {
|
||||
scope, err := r.authorize(ctx, obj.ID, probo.ActionThirdPartyRelationList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pageOrderBy := page.OrderBy[coredata.ThirdPartyOrderField]{
|
||||
Field: coredata.ThirdPartyOrderFieldName,
|
||||
Direction: page.OrderDirectionAsc,
|
||||
}
|
||||
if orderBy != nil {
|
||||
pageOrderBy = page.OrderBy[coredata.ThirdPartyOrderField]{
|
||||
Field: orderBy.Field,
|
||||
Direction: orderBy.Direction,
|
||||
}
|
||||
}
|
||||
|
||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
page, err := r.probo.ThirdParties.ListForParentThirdPartyID(ctx, scope, obj.ID, cursor)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot list child third parties", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewThirdPartyConnection(page, r, obj.ID), nil
|
||||
}
|
||||
|
||||
// Permission is the resolver for the permission field.
|
||||
func (r *thirdPartyResolver) Permission(ctx context.Context, obj *types.ThirdParty, action string) (bool, error) {
|
||||
return r.Resolver.Permission(ctx, obj, action)
|
||||
@@ -963,6 +1034,19 @@ func (r *thirdPartyConnectionResolver) TotalCount(ctx context.Context, obj *type
|
||||
return 0, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
case *thirdPartyResolver:
|
||||
if _, err := r.authorize(ctx, obj.ParentID, probo.ActionThirdPartyRelationList); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
count, err := r.probo.ThirdParties.CountForParentThirdPartyID(ctx, scope, obj.ParentID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot count child third parties", log.Error(err))
|
||||
|
||||
return 0, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
@@ -1145,3 +1229,15 @@ type thirdPartyContactResolver struct{ *Resolver }
|
||||
type thirdPartyDataPrivacyAgreementResolver struct{ *Resolver }
|
||||
type thirdPartyRiskAssessmentResolver struct{ *Resolver }
|
||||
type thirdPartyServiceResolver struct{ *Resolver }
|
||||
|
||||
// !!! WARNING !!!
|
||||
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
|
||||
// one last chance to move it out of harms way if you want. There are two reasons this happens:
|
||||
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
|
||||
// it when you're done.
|
||||
// - You have helper methods in this file. Move them out to keep these resolver files clean.
|
||||
/*
|
||||
func (r *mutationResolver) UncreateThirdPartyThirdPartyMapping(ctx context.Context, input types.UncreateThirdPartyThirdPartyMappingInput) (*types.UncreateThirdPartyThirdPartyMappingPayload, error) {
|
||||
panic(fmt.Errorf("not implemented: UncreateThirdPartyThirdPartyMapping - uncreateThirdPartyThirdPartyMapping"))
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -84,6 +84,7 @@ func NewThirdParty(v *coredata.ThirdParty) *ThirdParty {
|
||||
WebsiteURL: v.WebsiteURL,
|
||||
Category: v.Category,
|
||||
ShowOnTrustCenter: v.ShowOnTrustCenter,
|
||||
FirstLevel: v.FirstLevel,
|
||||
Countries: v.Countries,
|
||||
UpdatedAt: v.UpdatedAt,
|
||||
CreatedAt: v.CreatedAt,
|
||||
|
||||
@@ -70,7 +70,7 @@ func (r *Resolver) ListThirdPartiesTool(ctx context.Context, req *mcp.CallToolRe
|
||||
|
||||
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
|
||||
|
||||
thirdPartyFilter := coredata.NewThirdPartyFilter(nil)
|
||||
thirdPartyFilter := coredata.NewThirdPartyFilter(nil, input.FirstLevel)
|
||||
|
||||
page, err := prb.ThirdParties.ListForOrganizationID(ctx, scope, input.OrganizationID, cursor, thirdPartyFilter)
|
||||
if err != nil {
|
||||
@@ -6148,6 +6148,59 @@ func (r *Resolver) MoveTrackerResourceToCategoryTool(ctx context.Context, req *m
|
||||
return nil, types.MoveTrackerResourceToCategoryOutput{TrackerResource: types.NewTrackerResource(result.TrackerResource)}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) CreateThirdPartyThirdPartyMappingTool(ctx context.Context, req *mcp.CallToolRequest, input *types.CreateThirdPartyThirdPartyMappingInput) (*mcp.CallToolResult, types.CreateThirdPartyThirdPartyMappingOutput, error) {
|
||||
scope, err := r.Authorize(ctx, input.ParentThirdPartyID, probo.ActionThirdPartyRelationCreate)
|
||||
if err != nil {
|
||||
return nil, types.CreateThirdPartyThirdPartyMappingOutput{}, err
|
||||
}
|
||||
|
||||
if _, err := r.proboSvc.ThirdParties.CreateThirdPartyMapping(ctx, scope, input.ParentThirdPartyID, input.ChildThirdPartyID); err != nil {
|
||||
return nil, types.CreateThirdPartyThirdPartyMappingOutput{}, fmt.Errorf("cannot create third party mapping: %w", err)
|
||||
}
|
||||
|
||||
return nil, types.CreateThirdPartyThirdPartyMappingOutput{}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) DeleteThirdPartyThirdPartyMappingTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteThirdPartyThirdPartyMappingInput) (*mcp.CallToolResult, types.DeleteThirdPartyThirdPartyMappingOutput, error) {
|
||||
scope, err := r.Authorize(ctx, input.ParentThirdPartyID, probo.ActionThirdPartyRelationDelete)
|
||||
if err != nil {
|
||||
return nil, types.DeleteThirdPartyThirdPartyMappingOutput{}, err
|
||||
}
|
||||
|
||||
if err := r.proboSvc.ThirdParties.DeleteThirdPartyMapping(ctx, scope, input.ParentThirdPartyID, input.ChildThirdPartyID); err != nil {
|
||||
return nil, types.DeleteThirdPartyThirdPartyMappingOutput{}, fmt.Errorf("cannot delete third party mapping: %w", err)
|
||||
}
|
||||
|
||||
return nil, types.DeleteThirdPartyThirdPartyMappingOutput{}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) ListChildThirdPartiesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListChildThirdPartiesInput) (*mcp.CallToolResult, types.ListChildThirdPartiesOutput, error) {
|
||||
scope, err := r.Authorize(ctx, input.ParentThirdPartyID, probo.ActionThirdPartyRelationList)
|
||||
if err != nil {
|
||||
return nil, types.ListChildThirdPartiesOutput{}, err
|
||||
}
|
||||
|
||||
pageOrderBy := page.OrderBy[coredata.ThirdPartyOrderField]{
|
||||
Field: coredata.ThirdPartyOrderFieldCreatedAt,
|
||||
Direction: page.OrderDirectionDesc,
|
||||
}
|
||||
if input.OrderBy != nil {
|
||||
pageOrderBy = page.OrderBy[coredata.ThirdPartyOrderField]{
|
||||
Field: input.OrderBy.Field,
|
||||
Direction: input.OrderBy.Direction,
|
||||
}
|
||||
}
|
||||
|
||||
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
|
||||
|
||||
page, err := r.proboSvc.ThirdParties.ListForParentThirdPartyID(ctx, scope, input.ParentThirdPartyID, cursor)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot list child third parties: %w", err))
|
||||
}
|
||||
|
||||
return nil, types.NewListChildThirdPartiesOutput(page), nil
|
||||
}
|
||||
|
||||
func (r *Resolver) ListRiskAssessmentsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRiskAssessmentsInput) (*mcp.CallToolResult, types.ListRiskAssessmentsOutput, error) {
|
||||
scope, err := r.Authorize(ctx, input.OrganizationID, probo.ActionRiskAssessmentList)
|
||||
if err != nil {
|
||||
|
||||
@@ -627,6 +627,9 @@ components:
|
||||
organization_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Organization ID
|
||||
first_level:
|
||||
type: boolean
|
||||
description: Filter by first-level status
|
||||
order_by:
|
||||
$ref: "#/components/schemas/ThirdPartyOrderBy"
|
||||
description: ThirdParty order by
|
||||
@@ -650,6 +653,69 @@ components:
|
||||
items:
|
||||
$ref: "#/components/schemas/ThirdParty"
|
||||
|
||||
CreateThirdPartyThirdPartyMappingInput:
|
||||
type: object
|
||||
required:
|
||||
- parent_third_party_id
|
||||
- child_third_party_id
|
||||
properties:
|
||||
parent_third_party_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Parent third party ID
|
||||
child_third_party_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Child third party ID
|
||||
|
||||
CreateThirdPartyThirdPartyMappingOutput:
|
||||
type: object
|
||||
|
||||
DeleteThirdPartyThirdPartyMappingInput:
|
||||
type: object
|
||||
required:
|
||||
- parent_third_party_id
|
||||
- child_third_party_id
|
||||
properties:
|
||||
parent_third_party_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Parent third party ID
|
||||
child_third_party_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Child third party ID
|
||||
|
||||
DeleteThirdPartyThirdPartyMappingOutput:
|
||||
type: object
|
||||
|
||||
ListChildThirdPartiesInput:
|
||||
type: object
|
||||
required:
|
||||
- parent_third_party_id
|
||||
properties:
|
||||
parent_third_party_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Parent third party ID
|
||||
order_by:
|
||||
$ref: "#/components/schemas/ThirdPartyOrderBy"
|
||||
description: ThirdParty order by
|
||||
size:
|
||||
type: integer
|
||||
description: Page size
|
||||
cursor:
|
||||
$ref: "#/components/schemas/CursorKey"
|
||||
description: Page cursor
|
||||
|
||||
ListChildThirdPartiesOutput:
|
||||
type: object
|
||||
required:
|
||||
- thirdParties
|
||||
properties:
|
||||
next_cursor:
|
||||
$ref: "#/components/schemas/CursorKey"
|
||||
description: Next cursor
|
||||
thirdParties:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/ThirdParty"
|
||||
|
||||
ThirdParty:
|
||||
type: object
|
||||
required:
|
||||
@@ -657,6 +723,7 @@ components:
|
||||
- name
|
||||
- organization_id
|
||||
- category
|
||||
- first_level
|
||||
- created_at
|
||||
- updated_at
|
||||
properties:
|
||||
@@ -780,6 +847,9 @@ components:
|
||||
- string
|
||||
- "null"
|
||||
description: Trust page URL
|
||||
first_level:
|
||||
type: boolean
|
||||
description: Whether this is a first-level third party
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
@@ -11788,6 +11858,31 @@ tools:
|
||||
$ref: "#/components/schemas/ListThirdPartiesInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/ListThirdPartiesOutput"
|
||||
- name: createThirdPartyThirdPartyMapping
|
||||
description: Link a child third party to a parent third party
|
||||
hints:
|
||||
readonly: false
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/CreateThirdPartyThirdPartyMappingInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/CreateThirdPartyThirdPartyMappingOutput"
|
||||
- name: deleteThirdPartyThirdPartyMapping
|
||||
description: Unlink a child third party from a parent third party
|
||||
hints:
|
||||
readonly: false
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/DeleteThirdPartyThirdPartyMappingInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/DeleteThirdPartyThirdPartyMappingOutput"
|
||||
- name: listChildThirdParties
|
||||
description: List child third parties linked to a parent third party
|
||||
hints:
|
||||
readonly: true
|
||||
idempotent: true
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/ListChildThirdPartiesInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/ListChildThirdPartiesOutput"
|
||||
- name: listUsers
|
||||
description: List all users for the organization
|
||||
hints:
|
||||
|
||||
@@ -87,6 +87,7 @@ func NewThirdParty(v *coredata.ThirdParty) *ThirdParty {
|
||||
TermsOfServiceURL: v.TermsOfServiceURL,
|
||||
SecurityPageURL: v.SecurityPageURL,
|
||||
TrustPageURL: v.TrustPageURL,
|
||||
FirstLevel: v.FirstLevel,
|
||||
CreatedAt: v.CreatedAt,
|
||||
UpdatedAt: v.UpdatedAt,
|
||||
}
|
||||
@@ -111,6 +112,25 @@ func NewListThirdPartiesOutput(thirdPartyPage *page.Page[*coredata.ThirdParty, c
|
||||
}
|
||||
}
|
||||
|
||||
func NewListChildThirdPartiesOutput(thirdPartyPage *page.Page[*coredata.ThirdParty, coredata.ThirdPartyOrderField]) ListChildThirdPartiesOutput {
|
||||
thirdParties := make([]*ThirdParty, 0, len(thirdPartyPage.Data))
|
||||
for _, v := range thirdPartyPage.Data {
|
||||
thirdParties = append(thirdParties, NewThirdParty(v))
|
||||
}
|
||||
|
||||
var nextCursor *page.CursorKey
|
||||
|
||||
if len(thirdPartyPage.Data) > 0 {
|
||||
cursorKey := thirdPartyPage.Data[len(thirdPartyPage.Data)-1].CursorKey(thirdPartyPage.Cursor.OrderBy.Field)
|
||||
nextCursor = &cursorKey
|
||||
}
|
||||
|
||||
return ListChildThirdPartiesOutput{
|
||||
NextCursor: nextCursor,
|
||||
ThirdParties: thirdParties,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAddThirdPartyOutput(v *coredata.ThirdParty) AddThirdPartyOutput {
|
||||
return AddThirdPartyOutput{
|
||||
ThirdParty: NewThirdParty(v),
|
||||
|
||||
@@ -65,7 +65,7 @@ func (s ThirdPartyService) ListForOrganizationId(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
showOnTrustCenter := true
|
||||
filter := coredata.NewThirdPartyFilter(&showOnTrustCenter)
|
||||
filter := coredata.NewThirdPartyFilter(&showOnTrustCenter, nil)
|
||||
|
||||
err := thirdParties.LoadByOrganizationID(ctx, conn, scope, organizationID, cursor, filter)
|
||||
if err != nil {
|
||||
@@ -99,7 +99,7 @@ func (s ThirdPartyService) CountForTrustCenterId(
|
||||
|
||||
thirdParties := &coredata.ThirdParties{}
|
||||
showOnTrustCenter := true
|
||||
filter := coredata.NewThirdPartyFilter(&showOnTrustCenter)
|
||||
filter := coredata.NewThirdPartyFilter(&showOnTrustCenter, nil)
|
||||
|
||||
count, err = thirdParties.CountByOrganizationID(ctx, conn, scope, trustCenter.OrganizationID, filter)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user