Link measures to third parties
Add a many-to-many relationship between measures and third parties, surfaced as a measures tab on the third party detail page and a third parties tab on the measure detail page. Each side gets a paginated list with a link/unlink dialog. Also remove the right-hand drawer on the measure detail page and expose the state as a badge in the page header, mirroring how the compliance page surfaces its active flag. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
97
pkg/cmd/measure/link-third-party/link_third_party.go
vendored
Normal file
97
pkg/cmd/measure/link-third-party/link_third_party.go
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) 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 linkthirdparty
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.probo.inc/probo/pkg/cli/api"
|
||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||
)
|
||||
|
||||
const linkThirdPartyMutation = `
|
||||
mutation($input: CreateMeasureThirdPartyMappingInput!) {
|
||||
createMeasureThirdPartyMapping(input: $input) {
|
||||
measureEdge {
|
||||
node { id }
|
||||
}
|
||||
thirdPartyEdge {
|
||||
node { id }
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func NewCmdLinkThirdParty(f *cmdutil.Factory) *cobra.Command {
|
||||
var (
|
||||
flagMeasureID string
|
||||
flagThirdPartyID string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "link-third-party",
|
||||
Short: "Link a third party to a measure",
|
||||
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(
|
||||
linkThirdPartyMutation,
|
||||
map[string]any{
|
||||
"input": map[string]any{
|
||||
"measureId": flagMeasureID,
|
||||
"thirdPartyId": flagThirdPartyID,
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(
|
||||
f.IOStreams.Out,
|
||||
"Linked third party %s to measure %s\n",
|
||||
flagThirdPartyID,
|
||||
flagMeasureID,
|
||||
)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&flagMeasureID, "measure-id", "", "Measure ID (required)")
|
||||
cmd.Flags().StringVar(&flagThirdPartyID, "third-party-id", "", "Third party ID (required)")
|
||||
|
||||
_ = cmd.MarkFlagRequired("measure-id")
|
||||
_ = cmd.MarkFlagRequired("third-party-id")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -19,7 +19,9 @@ import (
|
||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||
"go.probo.inc/probo/pkg/cmd/measure/create"
|
||||
"go.probo.inc/probo/pkg/cmd/measure/delete"
|
||||
linkthirdparty "go.probo.inc/probo/pkg/cmd/measure/link-third-party"
|
||||
"go.probo.inc/probo/pkg/cmd/measure/list"
|
||||
unlinkthirdparty "go.probo.inc/probo/pkg/cmd/measure/unlink-third-party"
|
||||
"go.probo.inc/probo/pkg/cmd/measure/update"
|
||||
"go.probo.inc/probo/pkg/cmd/measure/view"
|
||||
)
|
||||
@@ -35,6 +37,8 @@ func NewCmdMeasure(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd.AddCommand(view.NewCmdView(f))
|
||||
cmd.AddCommand(update.NewCmdUpdate(f))
|
||||
cmd.AddCommand(delete.NewCmdDelete(f))
|
||||
cmd.AddCommand(linkthirdparty.NewCmdLinkThirdParty(f))
|
||||
cmd.AddCommand(unlinkthirdparty.NewCmdUnlinkThirdParty(f))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
93
pkg/cmd/measure/unlink-third-party/unlink_third_party.go
vendored
Normal file
93
pkg/cmd/measure/unlink-third-party/unlink_third_party.go
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) 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 unlinkthirdparty
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.probo.inc/probo/pkg/cli/api"
|
||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||
)
|
||||
|
||||
const unlinkThirdPartyMutation = `
|
||||
mutation($input: DeleteMeasureThirdPartyMappingInput!) {
|
||||
deleteMeasureThirdPartyMapping(input: $input) {
|
||||
deletedMeasureId
|
||||
deletedThirdPartyId
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func NewCmdUnlinkThirdParty(f *cmdutil.Factory) *cobra.Command {
|
||||
var (
|
||||
flagMeasureID string
|
||||
flagThirdPartyID string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "unlink-third-party",
|
||||
Short: "Unlink a third party from a measure",
|
||||
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(
|
||||
unlinkThirdPartyMutation,
|
||||
map[string]any{
|
||||
"input": map[string]any{
|
||||
"measureId": flagMeasureID,
|
||||
"thirdPartyId": flagThirdPartyID,
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(
|
||||
f.IOStreams.Out,
|
||||
"Unlinked third party %s from measure %s\n",
|
||||
flagThirdPartyID,
|
||||
flagMeasureID,
|
||||
)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&flagMeasureID, "measure-id", "", "Measure ID (required)")
|
||||
cmd.Flags().StringVar(&flagThirdPartyID, "third-party-id", "", "Third party ID (required)")
|
||||
|
||||
_ = cmd.MarkFlagRequired("measure-id")
|
||||
_ = cmd.MarkFlagRequired("third-party-id")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -720,3 +720,115 @@ WHERE %s
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Measures) CountByThirdPartyID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
thirdPartyID gid.GID,
|
||||
filter *MeasureFilter,
|
||||
) (int, error) {
|
||||
q := `
|
||||
WITH mtgtns AS (
|
||||
SELECT
|
||||
m.id,
|
||||
m.tenant_id,
|
||||
m.search_vector,
|
||||
m.state,
|
||||
m.category
|
||||
FROM
|
||||
measures m
|
||||
INNER JOIN
|
||||
measures_third_parties mtp ON m.id = mtp.measure_id
|
||||
WHERE
|
||||
mtp.third_party_id = @third_party_id
|
||||
)
|
||||
SELECT
|
||||
COUNT(id)
|
||||
FROM
|
||||
mtgtns
|
||||
WHERE %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||
|
||||
args := pgx.NamedArgs{"third_party_id": thirdPartyID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
|
||||
var count int
|
||||
if err := row.Scan(&count); err != nil {
|
||||
return 0, fmt.Errorf("cannot scan count: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (m *Measures) LoadByThirdPartyID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
thirdPartyID gid.GID,
|
||||
cursor *page.Cursor[MeasureOrderField],
|
||||
filter *MeasureFilter,
|
||||
) error {
|
||||
q := `
|
||||
WITH mtgtns AS (
|
||||
SELECT
|
||||
m.id,
|
||||
m.tenant_id,
|
||||
m.organization_id,
|
||||
m.category,
|
||||
m.name,
|
||||
m.description,
|
||||
m.state,
|
||||
m.reference_id,
|
||||
m.search_vector,
|
||||
m.created_at,
|
||||
m.updated_at
|
||||
FROM
|
||||
measures m
|
||||
INNER JOIN
|
||||
measures_third_parties mtp ON m.id = mtp.measure_id
|
||||
WHERE
|
||||
mtp.third_party_id = @third_party_id
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
category,
|
||||
name,
|
||||
description,
|
||||
state,
|
||||
reference_id,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
mtgtns
|
||||
WHERE %s
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.NamedArgs{"third_party_id": thirdPartyID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query measures: %w", err)
|
||||
}
|
||||
|
||||
measures, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Measure])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect measures: %w", err)
|
||||
}
|
||||
|
||||
*m = measures
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
114
pkg/coredata/measure_third_party.go
Normal file
114
pkg/coredata/measure_third_party.go
Normal file
@@ -0,0 +1,114 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
type (
|
||||
MeasureThirdParty struct {
|
||||
MeasureID gid.GID `db:"measure_id"`
|
||||
ThirdPartyID gid.GID `db:"third_party_id"`
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
|
||||
MeasureThirdParties []*MeasureThirdParty
|
||||
)
|
||||
|
||||
// Upsert links a measure to a third party. The organization_id stored in the
|
||||
// junction row is derived from the measures table inside the INSERT, so a
|
||||
// caller cannot place the mapping into a different organization than the
|
||||
// measure actually belongs to. Idempotent: re-linking an existing pair is a
|
||||
// no-op.
|
||||
func (mtp MeasureThirdParty) Upsert(
|
||||
ctx context.Context,
|
||||
conn pg.Tx,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO
|
||||
measures_third_parties (
|
||||
measure_id,
|
||||
third_party_id,
|
||||
organization_id,
|
||||
tenant_id,
|
||||
created_at
|
||||
)
|
||||
SELECT
|
||||
@measure_id,
|
||||
@third_party_id,
|
||||
m.organization_id,
|
||||
@tenant_id,
|
||||
@created_at
|
||||
FROM
|
||||
measures m
|
||||
WHERE
|
||||
m.id = @measure_id
|
||||
AND m.tenant_id = @tenant_id
|
||||
ON CONFLICT (measure_id, third_party_id) DO NOTHING;
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"measure_id": mtp.MeasureID,
|
||||
"third_party_id": mtp.ThirdPartyID,
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"created_at": mtp.CreatedAt,
|
||||
}
|
||||
|
||||
if _, err := conn.Exec(ctx, q, args); err != nil {
|
||||
return fmt.Errorf("cannot upsert measure third party: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mtp MeasureThirdParty) Delete(
|
||||
ctx context.Context,
|
||||
conn pg.Tx,
|
||||
scope Scoper,
|
||||
measureID gid.GID,
|
||||
thirdPartyID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
DELETE
|
||||
FROM
|
||||
measures_third_parties
|
||||
WHERE
|
||||
%s
|
||||
AND measure_id = @measure_id
|
||||
AND third_party_id = @third_party_id;
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"measure_id": measureID,
|
||||
"third_party_id": thirdPartyID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
|
||||
return err
|
||||
}
|
||||
22
pkg/coredata/migrations/20260522T120000Z.sql
Normal file
22
pkg/coredata/migrations/20260522T120000Z.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- 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.
|
||||
|
||||
CREATE TABLE measures_third_parties (
|
||||
measure_id TEXT NOT NULL REFERENCES measures(id) ON DELETE CASCADE,
|
||||
third_party_id TEXT NOT NULL REFERENCES third_parties(id) ON DELETE CASCADE,
|
||||
organization_id TEXT NOT NULL,
|
||||
tenant_id TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
PRIMARY KEY (measure_id, third_party_id)
|
||||
);
|
||||
@@ -1448,3 +1448,141 @@ LIMIT 1;
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *ThirdParties) CountByMeasureID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
measureID gid.GID,
|
||||
) (int, error) {
|
||||
q := `
|
||||
WITH tps AS (
|
||||
SELECT
|
||||
v.id,
|
||||
v.tenant_id
|
||||
FROM
|
||||
third_parties v
|
||||
INNER JOIN
|
||||
measures_third_parties mtp ON v.id = mtp.third_party_id
|
||||
WHERE
|
||||
mtp.measure_id = @measure_id
|
||||
)
|
||||
SELECT
|
||||
COUNT(id)
|
||||
FROM
|
||||
tps
|
||||
WHERE %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"measure_id": measureID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
|
||||
var count int
|
||||
|
||||
if err := row.Scan(&count); err != nil {
|
||||
return 0, fmt.Errorf("cannot count thirdParties: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (v *ThirdParties) LoadByMeasureID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
measureID gid.GID,
|
||||
cursor *page.Cursor[ThirdPartyOrderField],
|
||||
) error {
|
||||
q := `
|
||||
WITH tps AS (
|
||||
SELECT
|
||||
v.id,
|
||||
v.tenant_id,
|
||||
v.organization_id,
|
||||
v.common_third_party_id,
|
||||
v.name,
|
||||
v.description,
|
||||
v.category,
|
||||
v.headquarter_address,
|
||||
v.legal_name,
|
||||
v.website_url,
|
||||
v.privacy_policy_url,
|
||||
v.service_level_agreement_url,
|
||||
v.data_processing_agreement_url,
|
||||
v.business_associate_agreement_url,
|
||||
v.subprocessors_list_url,
|
||||
v.certifications,
|
||||
v.countries,
|
||||
v.business_owner_profile_id,
|
||||
v.security_owner_profile_id,
|
||||
v.status_page_url,
|
||||
v.terms_of_service_url,
|
||||
v.security_page_url,
|
||||
v.trust_page_url,
|
||||
v.show_on_trust_center,
|
||||
v.first_level,
|
||||
v.created_at,
|
||||
v.updated_at
|
||||
FROM
|
||||
third_parties v
|
||||
INNER JOIN
|
||||
measures_third_parties mtp ON v.id = mtp.third_party_id
|
||||
WHERE
|
||||
mtp.measure_id = @measure_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
|
||||
tps
|
||||
WHERE %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"measure_id": measureID}
|
||||
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 thirdParties: %w", err)
|
||||
}
|
||||
|
||||
thirdParties, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ThirdParty])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect thirdParties: %w", err)
|
||||
}
|
||||
|
||||
*v = thirdParties
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -22,23 +22,30 @@ type (
|
||||
ThirdPartyFilter struct {
|
||||
showOnTrustCenter *bool
|
||||
firstLevel *bool
|
||||
query *string
|
||||
}
|
||||
)
|
||||
|
||||
func NewThirdPartyFilter(showOnTrustCenter *bool, firstLevel *bool) *ThirdPartyFilter {
|
||||
func NewThirdPartyFilter(showOnTrustCenter *bool, firstLevel *bool, query *string) *ThirdPartyFilter {
|
||||
return &ThirdPartyFilter{
|
||||
showOnTrustCenter: showOnTrustCenter,
|
||||
firstLevel: firstLevel,
|
||||
query: query,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *ThirdPartyFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||
args := pgx.StrictNamedArgs{}
|
||||
args := pgx.StrictNamedArgs{
|
||||
"show_on_trust_center": nil,
|
||||
"filter_query": nil,
|
||||
}
|
||||
|
||||
if f.showOnTrustCenter != nil {
|
||||
args["show_on_trust_center"] = *f.showOnTrustCenter
|
||||
} else {
|
||||
args["show_on_trust_center"] = nil
|
||||
}
|
||||
|
||||
if f.query != nil && *f.query != "" {
|
||||
args["filter_query"] = *f.query
|
||||
}
|
||||
|
||||
if f.firstLevel != nil {
|
||||
@@ -58,13 +65,15 @@ func (f *ThirdPartyFilter) SQLFragment() string {
|
||||
show_on_trust_center = @show_on_trust_center::boolean
|
||||
ELSE TRUE
|
||||
END
|
||||
)
|
||||
AND
|
||||
(
|
||||
CASE
|
||||
AND CASE
|
||||
WHEN @first_level::boolean IS NOT NULL THEN
|
||||
first_level = @first_level::boolean
|
||||
ELSE TRUE
|
||||
END
|
||||
AND CASE
|
||||
WHEN @filter_query::text IS NOT NULL AND @filter_query::text <> '' THEN
|
||||
name ILIKE '%' || @filter_query || '%'
|
||||
ELSE TRUE
|
||||
END
|
||||
)`
|
||||
}
|
||||
|
||||
@@ -158,15 +158,17 @@ const (
|
||||
ActionControlObligationMappingDelete = "core:control:delete-obligation-mapping"
|
||||
|
||||
// Measure actions
|
||||
ActionMeasureGet = "core:measure:get"
|
||||
ActionMeasureList = "core:measure:list"
|
||||
ActionMeasureCreate = "core:measure:create"
|
||||
ActionMeasureUpdate = "core:measure:update"
|
||||
ActionMeasureDelete = "core:measure:delete"
|
||||
ActionMeasureEvidenceUpload = "core:measure:upload-evidence"
|
||||
ActionMeasureImport = "core:measure:import"
|
||||
ActionMeasureDocumentMappingCreate = "core:measure:create-document-mapping"
|
||||
ActionMeasureDocumentMappingDelete = "core:measure:delete-document-mapping"
|
||||
ActionMeasureGet = "core:measure:get"
|
||||
ActionMeasureList = "core:measure:list"
|
||||
ActionMeasureCreate = "core:measure:create"
|
||||
ActionMeasureUpdate = "core:measure:update"
|
||||
ActionMeasureDelete = "core:measure:delete"
|
||||
ActionMeasureEvidenceUpload = "core:measure:upload-evidence"
|
||||
ActionMeasureImport = "core:measure:import"
|
||||
ActionMeasureDocumentMappingCreate = "core:measure:create-document-mapping"
|
||||
ActionMeasureDocumentMappingDelete = "core:measure:delete-document-mapping"
|
||||
ActionMeasureThirdPartyMappingCreate = "core:measure:create-third-party-mapping"
|
||||
ActionMeasureThirdPartyMappingDelete = "core:measure:delete-third-party-mapping"
|
||||
|
||||
// Task actions
|
||||
ActionTaskGet = "core:task:get"
|
||||
|
||||
@@ -644,6 +644,138 @@ func (s MeasureService) CreateDocumentMapping(
|
||||
return measure, document, nil
|
||||
}
|
||||
|
||||
func (s MeasureService) CountForThirdPartyID(
|
||||
ctx context.Context, scope coredata.Scoper,
|
||||
thirdPartyID gid.GID,
|
||||
filter *coredata.MeasureFilter,
|
||||
) (int, error) {
|
||||
var count int
|
||||
|
||||
err := s.svc.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) (err error) {
|
||||
measures := &coredata.Measures{}
|
||||
|
||||
count, err = measures.CountByThirdPartyID(ctx, conn, scope, thirdPartyID, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count measures: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s MeasureService) ListForThirdPartyID(
|
||||
ctx context.Context, scope coredata.Scoper,
|
||||
thirdPartyID gid.GID,
|
||||
cursor *page.Cursor[coredata.MeasureOrderField],
|
||||
filter *coredata.MeasureFilter,
|
||||
) (*page.Page[*coredata.Measure, coredata.MeasureOrderField], error) {
|
||||
var measures coredata.Measures
|
||||
|
||||
thirdParty := &coredata.ThirdParty{}
|
||||
|
||||
err := s.svc.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
if err := thirdParty.LoadByID(ctx, conn, scope, thirdPartyID); err != nil {
|
||||
return fmt.Errorf("cannot load third party: %w", err)
|
||||
}
|
||||
|
||||
err := measures.LoadByThirdPartyID(ctx, conn, scope, thirdParty.ID, cursor, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load measures: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return page.NewPage(measures, cursor), nil
|
||||
}
|
||||
|
||||
func (s MeasureService) CreateThirdPartyMapping(
|
||||
ctx context.Context, scope coredata.Scoper,
|
||||
measureID gid.GID,
|
||||
thirdPartyID gid.GID,
|
||||
) (*coredata.Measure, *coredata.ThirdParty, error) {
|
||||
measure := &coredata.Measure{}
|
||||
thirdParty := &coredata.ThirdParty{}
|
||||
|
||||
err := s.svc.pg.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
if err := measure.LoadByID(ctx, tx, scope, measureID); err != nil {
|
||||
return fmt.Errorf("cannot load measure: %w", err)
|
||||
}
|
||||
|
||||
if err := thirdParty.LoadByID(ctx, tx, scope, thirdPartyID); err != nil {
|
||||
return fmt.Errorf("cannot load third party: %w", err)
|
||||
}
|
||||
|
||||
measureThirdParty := &coredata.MeasureThirdParty{
|
||||
MeasureID: measure.ID,
|
||||
ThirdPartyID: thirdParty.ID,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := measureThirdParty.Upsert(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot upsert measure third party: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return measure, thirdParty, nil
|
||||
}
|
||||
|
||||
func (s MeasureService) DeleteThirdPartyMapping(
|
||||
ctx context.Context, scope coredata.Scoper,
|
||||
measureID gid.GID,
|
||||
thirdPartyID gid.GID,
|
||||
) (*coredata.Measure, *coredata.ThirdParty, error) {
|
||||
measure := &coredata.Measure{}
|
||||
thirdParty := &coredata.ThirdParty{}
|
||||
|
||||
err := s.svc.pg.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
if err := measure.LoadByID(ctx, tx, scope, measureID); err != nil {
|
||||
return fmt.Errorf("cannot load measure: %w", err)
|
||||
}
|
||||
|
||||
if err := thirdParty.LoadByID(ctx, tx, scope, thirdPartyID); err != nil {
|
||||
return fmt.Errorf("cannot load third party: %w", err)
|
||||
}
|
||||
|
||||
measureThirdParty := &coredata.MeasureThirdParty{}
|
||||
if err := measureThirdParty.Delete(ctx, tx, scope, measure.ID, thirdParty.ID); err != nil {
|
||||
return fmt.Errorf("cannot delete measure third party mapping: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return measure, thirdParty, nil
|
||||
}
|
||||
|
||||
func (s MeasureService) DeleteDocumentMapping(
|
||||
ctx context.Context, scope coredata.Scoper,
|
||||
measureID gid.GID,
|
||||
|
||||
@@ -211,14 +211,18 @@ func (cvrar *CreateThirdPartyRiskAssessmentRequest) Validate() error {
|
||||
func (s ThirdPartyService) CountForOrganizationID(
|
||||
ctx context.Context, scope coredata.Scoper,
|
||||
organizationID gid.GID,
|
||||
filter *coredata.ThirdPartyFilter,
|
||||
) (int, error) {
|
||||
var count int
|
||||
|
||||
if filter == nil {
|
||||
filter = coredata.NewThirdPartyFilter(nil, nil, nil)
|
||||
}
|
||||
|
||||
err := s.svc.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) (err error) {
|
||||
thirdParties := coredata.ThirdParties{}
|
||||
filter := &coredata.ThirdPartyFilter{}
|
||||
|
||||
count, err = thirdParties.CountByOrganizationID(ctx, conn, scope, organizationID, filter)
|
||||
if err != nil {
|
||||
@@ -269,6 +273,68 @@ func (s ThirdPartyService) ListForOrganizationID(
|
||||
return page.NewPage(thirdParties, cursor), nil
|
||||
}
|
||||
|
||||
func (s ThirdPartyService) CountForMeasureID(
|
||||
ctx context.Context, scope coredata.Scoper,
|
||||
measureID 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.CountByMeasureID(ctx, conn, scope, measureID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count thirdParties: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s ThirdPartyService) ListForMeasureID(
|
||||
ctx context.Context, scope coredata.Scoper,
|
||||
measureID gid.GID,
|
||||
cursor *page.Cursor[coredata.ThirdPartyOrderField],
|
||||
) (*page.Page[*coredata.ThirdParty, coredata.ThirdPartyOrderField], error) {
|
||||
var thirdParties coredata.ThirdParties
|
||||
|
||||
measure := &coredata.Measure{}
|
||||
|
||||
err := s.svc.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
if err := measure.LoadByID(ctx, conn, scope, measureID); err != nil {
|
||||
return fmt.Errorf("cannot load measure: %w", err)
|
||||
}
|
||||
|
||||
if err := thirdParties.LoadByMeasureID(
|
||||
ctx,
|
||||
conn,
|
||||
scope,
|
||||
measure.ID,
|
||||
cursor,
|
||||
); err != nil {
|
||||
return fmt.Errorf("cannot load thirdParties: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return page.NewPage(thirdParties, cursor), nil
|
||||
}
|
||||
|
||||
func (s ThirdPartyService) CountForDatumID(
|
||||
ctx context.Context, scope coredata.Scoper,
|
||||
datumID gid.GID,
|
||||
|
||||
@@ -72,7 +72,7 @@ func (r *assetResolver) ThirdParties(ctx context.Context, obj *types.Asset, firs
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewThirdPartyConnection(page, r, obj.ID), nil
|
||||
return types.NewThirdPartyConnection(page, r, obj.ID, nil), nil
|
||||
}
|
||||
|
||||
// Organization is the resolver for the organization field.
|
||||
@@ -177,7 +177,7 @@ func (r *datumResolver) ThirdParties(ctx context.Context, obj *types.Datum, firs
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewThirdPartyConnection(page, r, obj.ID), nil
|
||||
return types.NewThirdPartyConnection(page, r, obj.ID, nil), nil
|
||||
}
|
||||
|
||||
// Organization is the resolver for the organization field.
|
||||
|
||||
@@ -95,6 +95,14 @@ type Measure implements Node {
|
||||
filter: DocumentFilter
|
||||
): DocumentConnection! @goField(forceResolver: true)
|
||||
|
||||
thirdParties(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: ThirdPartyOrder
|
||||
): ThirdPartyConnection! @goField(forceResolver: true)
|
||||
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
|
||||
@@ -126,6 +134,12 @@ extend type Mutation {
|
||||
deleteMeasureDocumentMapping(
|
||||
input: DeleteMeasureDocumentMappingInput!
|
||||
): DeleteMeasureDocumentMappingPayload!
|
||||
createMeasureThirdPartyMapping(
|
||||
input: CreateMeasureThirdPartyMappingInput!
|
||||
): CreateMeasureThirdPartyMappingPayload!
|
||||
deleteMeasureThirdPartyMapping(
|
||||
input: DeleteMeasureThirdPartyMappingInput!
|
||||
): DeleteMeasureThirdPartyMappingPayload!
|
||||
}
|
||||
|
||||
input CreateMeasureInput {
|
||||
@@ -187,3 +201,23 @@ type DeleteMeasureDocumentMappingPayload {
|
||||
deletedMeasureId: ID!
|
||||
deletedDocumentId: ID!
|
||||
}
|
||||
|
||||
input CreateMeasureThirdPartyMappingInput {
|
||||
measureId: ID!
|
||||
thirdPartyId: ID!
|
||||
}
|
||||
|
||||
input DeleteMeasureThirdPartyMappingInput {
|
||||
measureId: ID!
|
||||
thirdPartyId: ID!
|
||||
}
|
||||
|
||||
type CreateMeasureThirdPartyMappingPayload {
|
||||
measureEdge: MeasureEdge!
|
||||
thirdPartyEdge: ThirdPartyEdge!
|
||||
}
|
||||
|
||||
type DeleteMeasureThirdPartyMappingPayload {
|
||||
deletedMeasureId: ID!
|
||||
deletedThirdPartyId: ID!
|
||||
}
|
||||
|
||||
@@ -179,6 +179,7 @@ input ThirdPartyOrder
|
||||
|
||||
input ThirdPartyFilter {
|
||||
firstLevel: Boolean
|
||||
query: String
|
||||
}
|
||||
|
||||
input ThirdPartyComplianceReportOrder
|
||||
@@ -255,6 +256,15 @@ type ThirdParty implements Node {
|
||||
orderBy: ThirdPartyRiskAssessmentOrder
|
||||
): ThirdPartyRiskAssessmentConnection! @goField(forceResolver: true)
|
||||
|
||||
measures(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: MeasureOrder
|
||||
filter: MeasureFilter
|
||||
): MeasureConnection! @goField(forceResolver: true)
|
||||
|
||||
businessOwner: Profile @goField(forceResolver: true)
|
||||
securityOwner: Profile @goField(forceResolver: true)
|
||||
|
||||
|
||||
@@ -188,6 +188,35 @@ func (r *measureResolver) Documents(ctx context.Context, obj *types.Measure, fir
|
||||
return types.NewDocumentConnection(pg, r, obj.ID, documentFilter), nil
|
||||
}
|
||||
|
||||
// ThirdParties is the resolver for the thirdParties field.
|
||||
func (r *measureResolver) ThirdParties(ctx context.Context, obj *types.Measure, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ThirdPartyOrderBy) (*types.ThirdPartyConnection, error) {
|
||||
scope, err := r.authorize(ctx, obj.ID, probo.ActionThirdPartyList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pageOrderBy := page.OrderBy[coredata.ThirdPartyOrderField]{
|
||||
Field: coredata.ThirdPartyOrderFieldCreatedAt,
|
||||
Direction: page.OrderDirectionDesc,
|
||||
}
|
||||
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.ListForMeasureID(ctx, scope, obj.ID, cursor)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot list measure third parties", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewThirdPartyConnection(page, r, obj.ID, nil), nil
|
||||
}
|
||||
|
||||
// Permission is the resolver for the permission field.
|
||||
func (r *measureResolver) Permission(ctx context.Context, obj *types.Measure, action string) (bool, error) {
|
||||
return r.Resolver.Permission(ctx, obj, action)
|
||||
@@ -224,6 +253,14 @@ func (r *measureConnectionResolver) TotalCount(ctx context.Context, obj *types.M
|
||||
return 0, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
case *thirdPartyResolver:
|
||||
count, err := r.probo.Measures.CountForThirdPartyID(ctx, scope, obj.ParentID, obj.Filters)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot count measures", log.Error(err))
|
||||
return 0, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
@@ -388,6 +425,44 @@ func (r *mutationResolver) DeleteMeasureDocumentMapping(ctx context.Context, inp
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateMeasureThirdPartyMapping is the resolver for the createMeasureThirdPartyMapping field.
|
||||
func (r *mutationResolver) CreateMeasureThirdPartyMapping(ctx context.Context, input types.CreateMeasureThirdPartyMappingInput) (*types.CreateMeasureThirdPartyMappingPayload, error) {
|
||||
scope, err := r.authorize(ctx, input.MeasureID, probo.ActionMeasureThirdPartyMappingCreate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
measure, thirdParty, err := r.probo.Measures.CreateThirdPartyMapping(ctx, scope, input.MeasureID, input.ThirdPartyID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot create measure third party mapping", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return &types.CreateMeasureThirdPartyMappingPayload{
|
||||
MeasureEdge: types.NewMeasureEdge(measure, coredata.MeasureOrderFieldCreatedAt),
|
||||
ThirdPartyEdge: types.NewThirdPartyEdge(thirdParty, coredata.ThirdPartyOrderFieldCreatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteMeasureThirdPartyMapping is the resolver for the deleteMeasureThirdPartyMapping field.
|
||||
func (r *mutationResolver) DeleteMeasureThirdPartyMapping(ctx context.Context, input types.DeleteMeasureThirdPartyMappingInput) (*types.DeleteMeasureThirdPartyMappingPayload, error) {
|
||||
scope, err := r.authorize(ctx, input.MeasureID, probo.ActionMeasureThirdPartyMappingDelete)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
measure, thirdParty, err := r.probo.Measures.DeleteThirdPartyMapping(ctx, scope, input.MeasureID, input.ThirdPartyID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot delete measure third party mapping", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return &types.DeleteMeasureThirdPartyMappingPayload{
|
||||
DeletedMeasureID: measure.ID,
|
||||
DeletedThirdPartyID: thirdParty.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Measure returns schema.MeasureResolver implementation.
|
||||
func (r *Resolver) Measure() schema.MeasureResolver { return &measureResolver{r} }
|
||||
|
||||
|
||||
@@ -1291,12 +1291,16 @@ func (r *organizationResolver) ThirdParties(ctx context.Context, obj *types.Orga
|
||||
|
||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
var firstLevel *bool
|
||||
var (
|
||||
firstLevel *bool
|
||||
query *string
|
||||
)
|
||||
if filter != nil {
|
||||
firstLevel = filter.FirstLevel
|
||||
query = filter.Query
|
||||
}
|
||||
|
||||
thirdPartyFilter := coredata.NewThirdPartyFilter(nil, firstLevel)
|
||||
thirdPartyFilter := coredata.NewThirdPartyFilter(nil, firstLevel, query)
|
||||
|
||||
page, err := r.probo.ThirdParties.ListForOrganizationID(ctx, scope, obj.ID, cursor, thirdPartyFilter)
|
||||
if err != nil {
|
||||
@@ -1304,7 +1308,7 @@ func (r *organizationResolver) ThirdParties(ctx context.Context, obj *types.Orga
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewThirdPartyConnection(page, r, obj.ID), nil
|
||||
return types.NewThirdPartyConnection(page, r, obj.ID, thirdPartyFilter), nil
|
||||
}
|
||||
|
||||
// ThirdPartiesDocument is the resolver for the thirdPartiesDocument field.
|
||||
|
||||
@@ -223,7 +223,7 @@ func (r *processingActivityResolver) ThirdParties(ctx context.Context, obj *type
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewThirdPartyConnection(page, r, obj.ID), nil
|
||||
return types.NewThirdPartyConnection(page, r, obj.ID, nil), nil
|
||||
}
|
||||
|
||||
// DataProtectionImpactAssessment is the resolver for the dataProtectionImpactAssessment field.
|
||||
|
||||
@@ -820,6 +820,40 @@ func (r *thirdPartyResolver) RiskAssessments(ctx context.Context, obj *types.Thi
|
||||
return types.NewThirdPartyRiskAssessmentConnection(page), nil
|
||||
}
|
||||
|
||||
// Measures is the resolver for the measures field.
|
||||
func (r *thirdPartyResolver) Measures(ctx context.Context, obj *types.ThirdParty, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MeasureOrderBy, filter *types.MeasureFilter) (*types.MeasureConnection, error) {
|
||||
scope, err := r.authorize(ctx, obj.ID, probo.ActionMeasureList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pageOrderBy := page.OrderBy[coredata.MeasureOrderField]{
|
||||
Field: coredata.MeasureOrderFieldCreatedAt,
|
||||
Direction: page.OrderDirectionDesc,
|
||||
}
|
||||
if orderBy != nil {
|
||||
pageOrderBy = page.OrderBy[coredata.MeasureOrderField]{
|
||||
Field: orderBy.Field,
|
||||
Direction: orderBy.Direction,
|
||||
}
|
||||
}
|
||||
|
||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
var measureFilter = coredata.NewMeasureFilter(nil, nil, nil)
|
||||
if filter != nil {
|
||||
measureFilter = coredata.NewMeasureFilter(filter.Query, filter.State, filter.Category)
|
||||
}
|
||||
|
||||
page, err := r.probo.Measures.ListForThirdPartyID(ctx, scope, obj.ID, cursor, measureFilter)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot list third party measures", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewMeasureConnection(page, r, obj.ID, measureFilter), nil
|
||||
}
|
||||
|
||||
// BusinessOwner is the resolver for the businessOwner field.
|
||||
func (r *thirdPartyResolver) BusinessOwner(ctx context.Context, obj *types.ThirdParty) (*types.Profile, error) {
|
||||
if _, err := r.authorize(ctx, obj.ID, iam.ActionMembershipProfileGet); err != nil {
|
||||
@@ -898,7 +932,7 @@ func (r *thirdPartyResolver) ChildThirdParties(ctx context.Context, obj *types.T
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewThirdPartyConnection(page, r, obj.ID), nil
|
||||
return types.NewThirdPartyConnection(page, r, obj.ID, nil), nil
|
||||
}
|
||||
|
||||
// Permission is the resolver for the permission field.
|
||||
@@ -1012,7 +1046,7 @@ func (r *thirdPartyConnectionResolver) TotalCount(ctx context.Context, obj *type
|
||||
|
||||
switch obj.Resolver.(type) {
|
||||
case *organizationResolver:
|
||||
count, err := r.probo.ThirdParties.CountForOrganizationID(ctx, scope, obj.ParentID)
|
||||
count, err := r.probo.ThirdParties.CountForOrganizationID(ctx, scope, obj.ParentID, obj.Filters)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot count thirdParties", log.Error(err))
|
||||
return 0, gqlutils.Internal(ctx)
|
||||
@@ -1043,7 +1077,14 @@ func (r *thirdPartyConnectionResolver) TotalCount(ctx context.Context, obj *type
|
||||
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
|
||||
case *measureResolver:
|
||||
count, err := r.probo.ThirdParties.CountForMeasureID(ctx, scope, obj.ParentID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot count thirdParties", log.Error(err))
|
||||
return 0, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
@@ -1229,15 +1270,3 @@ 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"))
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -31,6 +31,7 @@ type (
|
||||
|
||||
Resolver any
|
||||
ParentID gid.GID
|
||||
Filters *coredata.ThirdPartyFilter
|
||||
}
|
||||
)
|
||||
|
||||
@@ -38,6 +39,7 @@ func NewThirdPartyConnection(
|
||||
p *page.Page[*coredata.ThirdParty, coredata.ThirdPartyOrderField],
|
||||
parentType any,
|
||||
parentID gid.GID,
|
||||
filters *coredata.ThirdPartyFilter,
|
||||
) *ThirdPartyConnection {
|
||||
var edges = make([]*ThirdPartyEdge, len(p.Data))
|
||||
|
||||
@@ -51,6 +53,7 @@ func NewThirdPartyConnection(
|
||||
|
||||
Resolver: parentType,
|
||||
ParentID: parentID,
|
||||
Filters: filters,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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, input.FirstLevel)
|
||||
thirdPartyFilter := coredata.NewThirdPartyFilter(nil, input.FirstLevel, nil)
|
||||
|
||||
page, err := prb.ThirdParties.ListForOrganizationID(ctx, scope, input.OrganizationID, cursor, thirdPartyFilter)
|
||||
if err != nil {
|
||||
@@ -2664,6 +2664,14 @@ func (r *Resolver) LinkMeasureTool(ctx context.Context, req *mcp.CallToolRequest
|
||||
if _, _, err := svc.Measures.CreateDocumentMapping(ctx, scope, input.MeasureID, input.ResourceID); err != nil {
|
||||
return nil, types.LinkMeasureOutput{}, fmt.Errorf("failed to link measure to document: %w", err)
|
||||
}
|
||||
case coredata.ThirdPartyEntityType:
|
||||
if _, err := r.Authorize(ctx, input.MeasureID, probo.ActionMeasureThirdPartyMappingCreate); err != nil {
|
||||
return nil, types.LinkMeasureOutput{}, err
|
||||
}
|
||||
|
||||
if _, _, err := svc.Measures.CreateThirdPartyMapping(ctx, scope, input.MeasureID, input.ResourceID); err != nil {
|
||||
return nil, types.LinkMeasureOutput{}, fmt.Errorf("failed to link measure to third party: %w", err)
|
||||
}
|
||||
default:
|
||||
return nil, types.LinkMeasureOutput{}, fmt.Errorf("unsupported resource type for measure linking: entity type %d", input.ResourceID.EntityType())
|
||||
}
|
||||
@@ -2700,6 +2708,14 @@ func (r *Resolver) UnlinkMeasureTool(ctx context.Context, req *mcp.CallToolReque
|
||||
if _, _, err := svc.Measures.DeleteDocumentMapping(ctx, scope, input.MeasureID, input.ResourceID); err != nil {
|
||||
return nil, types.UnlinkMeasureOutput{}, fmt.Errorf("failed to unlink measure from document: %w", err)
|
||||
}
|
||||
case coredata.ThirdPartyEntityType:
|
||||
if _, err := r.Authorize(ctx, input.MeasureID, probo.ActionMeasureThirdPartyMappingDelete); err != nil {
|
||||
return nil, types.UnlinkMeasureOutput{}, err
|
||||
}
|
||||
|
||||
if _, _, err := svc.Measures.DeleteThirdPartyMapping(ctx, scope, input.MeasureID, input.ResourceID); err != nil {
|
||||
return nil, types.UnlinkMeasureOutput{}, fmt.Errorf("failed to unlink measure from third party: %w", err)
|
||||
}
|
||||
default:
|
||||
return nil, types.UnlinkMeasureOutput{}, fmt.Errorf("unsupported resource type for measure unlinking: entity type %d", input.ResourceID.EntityType())
|
||||
}
|
||||
|
||||
@@ -2396,7 +2396,7 @@ components:
|
||||
description: Measure ID
|
||||
resource_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: ID of the resource to link (control, risk, or document)
|
||||
description: ID of the resource to link (control, risk, document, or third party)
|
||||
|
||||
LinkMeasureOutput:
|
||||
type: object
|
||||
@@ -2412,7 +2412,7 @@ components:
|
||||
description: Measure ID
|
||||
resource_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: ID of the resource to unlink (control, risk, or document)
|
||||
description: ID of the resource to unlink (control, risk, document, or third party)
|
||||
|
||||
UnlinkMeasureOutput:
|
||||
type: object
|
||||
@@ -12182,7 +12182,7 @@ tools:
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/ListMeasureEvidencesOutput"
|
||||
- name: linkMeasure
|
||||
description: Link a measure to a resource (control, risk, or document). The resource type is determined from the resource_id GID.
|
||||
description: Link a measure to a resource (control, risk, document, or third party). The resource type is determined from the resource_id GID.
|
||||
hints:
|
||||
readonly: false
|
||||
inputSchema:
|
||||
@@ -12190,7 +12190,7 @@ tools:
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/LinkMeasureOutput"
|
||||
- name: unlinkMeasure
|
||||
description: Unlink a measure from a resource (control, risk, or document). The resource type is determined from the resource_id GID.
|
||||
description: Unlink a measure from a resource (control, risk, document, or third party). The resource type is determined from the resource_id GID.
|
||||
hints:
|
||||
readonly: false
|
||||
inputSchema:
|
||||
|
||||
@@ -65,7 +65,7 @@ func (s ThirdPartyService) ListForOrganizationId(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
showOnTrustCenter := true
|
||||
filter := coredata.NewThirdPartyFilter(&showOnTrustCenter, nil)
|
||||
filter := coredata.NewThirdPartyFilter(&showOnTrustCenter, nil, 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, nil)
|
||||
filter := coredata.NewThirdPartyFilter(&showOnTrustCenter, nil, nil)
|
||||
|
||||
count, err = thirdParties.CountByOrganizationID(ctx, conn, scope, trustCenter.OrganizationID, filter)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user