Scope sub-third-parties per parent
Replace the many-to-many junction table with a direct parent_third_party_id foreign key on third_parties. Each sub-third-party now belongs to exactly one parent, making duplicates across parents independent entities. Replace the firstLevel boolean with an integer level field (1 = direct, 2+ = parent level + 1) to support arbitrary nesting depth. Remove the createThirdPartyThirdPartyMapping and deleteThirdPartyThirdPartyMapping mutations, the CLI link/unlink commands, and the corresponding MCP tools. Creating a child third party now just requires passing parentThirdPartyId on the existing createThirdParty mutation. The frontend walks the parentThirdParty chain to build display names like "Name (Ancestor1/Ancestor2)" and shows clickable ancestor links on the detail page. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
251
pkg/coredata/migrations/20260608T120000Z.sql
Normal file
251
pkg/coredata/migrations/20260608T120000Z.sql
Normal file
@@ -0,0 +1,251 @@
|
||||
-- Copyright (c) 2025-2026 Probo Inc <hello@probo.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.
|
||||
|
||||
-- ADD COLUMN ... DEFAULT 1 already backfills every existing row to level 1, so
|
||||
-- only the sub-third-parties (first_level = false) need correcting to level 2.
|
||||
-- Scoping the UPDATE to them leaves first-level rows entirely untouched.
|
||||
ALTER TABLE third_parties ADD COLUMN level integer NOT NULL DEFAULT 1;
|
||||
UPDATE third_parties SET level = 2 WHERE first_level = false;
|
||||
ALTER TABLE third_parties ALTER COLUMN level DROP DEFAULT;
|
||||
|
||||
-- Keep first_level around instead of dropping it so the change is reversible and
|
||||
-- old code paths keep working during a rolling deploy. New inserts no longer set
|
||||
-- it, so give it a default to satisfy the NOT NULL constraint.
|
||||
ALTER TABLE third_parties ALTER COLUMN first_level SET DEFAULT true;
|
||||
|
||||
ALTER TABLE third_parties ADD COLUMN parent_third_party_id text REFERENCES third_parties(id) ON DELETE CASCADE;
|
||||
|
||||
-- Pick each non-first-level child's primary parent: the parent it will be
|
||||
-- re-parented to in place. Only first_level = false rows are eligible — a
|
||||
-- first-level row stays a root (its links become copies below, never an
|
||||
-- in-place move). Prefer a top-level (first_level = true) parent so the child
|
||||
-- anchors to a real root, then fall back to the relationship that was created
|
||||
-- first. IDs are text GIDs, so MIN() would order them lexicographically rather
|
||||
-- than chronologically — sort by the junction row's created_at instead, with
|
||||
-- the parent id as a deterministic tie-break.
|
||||
CREATE TEMP TABLE _tp_primary_parent AS
|
||||
SELECT DISTINCT ON (tpr.child_third_party_id)
|
||||
tpr.child_third_party_id,
|
||||
tpr.parent_third_party_id
|
||||
FROM third_party_third_parties tpr
|
||||
JOIN third_parties parent ON parent.id = tpr.parent_third_party_id
|
||||
JOIN third_parties child ON child.id = tpr.child_third_party_id
|
||||
WHERE child.first_level = false
|
||||
ORDER BY
|
||||
tpr.child_third_party_id,
|
||||
parent.first_level DESC,
|
||||
tpr.created_at ASC,
|
||||
tpr.parent_third_party_id ASC;
|
||||
|
||||
-- Assign every existing child its primary parent in place. Updating the row in
|
||||
-- place preserves its ID and therefore every dependent record (risk
|
||||
-- assessments, services, contacts, measure links, …) — nothing is deleted or
|
||||
-- re-keyed for the common one-parent case.
|
||||
-- Restricted to first_level = false: only sub-third-parties may gain a parent;
|
||||
-- a top-level (first_level = true) row must stay at level 1 with no parent even
|
||||
-- if it appears in the junction table by accident.
|
||||
UPDATE third_parties tp
|
||||
SET parent_third_party_id = primary_parent.parent_third_party_id
|
||||
FROM _tp_primary_parent AS primary_parent
|
||||
WHERE tp.id = primary_parent.child_third_party_id
|
||||
AND tp.first_level = false;
|
||||
|
||||
-- Re-qualify the names of the reparented sub-third-parties to the hierarchy
|
||||
-- convention used by the console/vetting ("base (root/.../parent)"), matching
|
||||
-- the copies created below. Names are rebuilt from base names (trailing " (…)"
|
||||
-- stripped) so already-qualified rows are normalized rather than double-suffixed.
|
||||
WITH RECURSIVE tp_path AS (
|
||||
SELECT
|
||||
id,
|
||||
trim(regexp_replace(name, '\s*\([^)]*\)\s*$', '')) AS path
|
||||
FROM third_parties
|
||||
WHERE parent_third_party_id IS NULL
|
||||
UNION ALL
|
||||
SELECT
|
||||
c.id,
|
||||
p.path || '/' || trim(regexp_replace(c.name, '\s*\([^)]*\)\s*$', '')) AS path
|
||||
FROM third_parties c
|
||||
JOIN tp_path p ON p.id = c.parent_third_party_id
|
||||
)
|
||||
UPDATE third_parties tp
|
||||
SET name = trim(regexp_replace(tp.name, '\s*\([^)]*\)\s*$', '')) || ' (' || pp.path || ')'
|
||||
FROM tp_path pp
|
||||
WHERE pp.id = tp.parent_third_party_id
|
||||
AND tp.first_level = false;
|
||||
|
||||
-- Every junction row that is NOT an in-place reparent becomes its own copy: the
|
||||
-- extra parents of a non-first-level child, plus every link whose child is
|
||||
-- first_level = true (the root is preserved and a fresh sub-third-party copy is
|
||||
-- created under the parent). Generate a fresh GID for each (child, parent) pair.
|
||||
-- generate_gid() and parse_tenant_id() are defined in migration 20250420T120000Z.
|
||||
CREATE TEMP TABLE _tp_copy_map AS
|
||||
SELECT
|
||||
tpr.child_third_party_id AS old_id,
|
||||
tpr.parent_third_party_id AS parent_id,
|
||||
generate_gid(parse_tenant_id(tp.tenant_id), 7) AS new_id
|
||||
FROM third_party_third_parties tpr
|
||||
JOIN third_parties tp ON tp.id = tpr.child_third_party_id
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM _tp_primary_parent pp
|
||||
WHERE pp.child_third_party_id = tpr.child_third_party_id
|
||||
AND pp.parent_third_party_id = tpr.parent_third_party_id
|
||||
);
|
||||
|
||||
-- Resolve each third party's hierarchy-qualified path (root → self, base names
|
||||
-- joined by "/"), mirroring the console/vetting naming convention. A copy under
|
||||
-- parent P is named "<child base> (<path of P>)", e.g. "Google Workspace (Probo)".
|
||||
-- The base name strips any trailing " (…)" suffix exactly like the app regex.
|
||||
WITH RECURSIVE tp_path AS (
|
||||
SELECT
|
||||
id,
|
||||
level,
|
||||
trim(regexp_replace(name, '\s*\([^)]*\)\s*$', '')) AS path
|
||||
FROM third_parties
|
||||
WHERE parent_third_party_id IS NULL
|
||||
UNION ALL
|
||||
SELECT
|
||||
c.id,
|
||||
c.level,
|
||||
p.path || '/' || trim(regexp_replace(c.name, '\s*\([^)]*\)\s*$', '')) AS path
|
||||
FROM third_parties c
|
||||
JOIN tp_path p ON p.id = c.parent_third_party_id
|
||||
)
|
||||
INSERT INTO third_parties (
|
||||
id,
|
||||
tenant_id,
|
||||
organization_id,
|
||||
parent_third_party_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,
|
||||
level,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
m.new_id,
|
||||
tp.tenant_id,
|
||||
tp.organization_id,
|
||||
-- The parent is always an existing row that needs no remapping.
|
||||
m.parent_id,
|
||||
-- A copy is a relationship/subprocessor record, not the canonical
|
||||
-- catalog-linked vendor. Leave common_third_party_id NULL so the original
|
||||
-- root stays the single third party resolved for a common catalog entry
|
||||
-- (e.g. cookie-banner tracker mapping by common_third_party_id).
|
||||
NULL,
|
||||
-- Hierarchy-qualified name: child base name suffixed with the parent's path.
|
||||
trim(regexp_replace(tp.name, '\s*\([^)]*\)\s*$', '')) || ' (' || pp.path || ')',
|
||||
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,
|
||||
-- Copies are sub-third-parties (level >= 2), never first-level roots.
|
||||
false,
|
||||
-- Level follows the parent, not the copied child: a first-level child
|
||||
-- (level 1) copied under Probo (level 1) becomes a level-2 sub-third-party.
|
||||
pp.level + 1,
|
||||
tp.created_at,
|
||||
tp.updated_at
|
||||
FROM _tp_copy_map m
|
||||
JOIN third_parties tp ON tp.id = m.old_id
|
||||
JOIN tp_path pp ON pp.id = m.parent_id;
|
||||
|
||||
INSERT INTO third_party_services (
|
||||
tenant_id,
|
||||
id,
|
||||
organization_id,
|
||||
third_party_id,
|
||||
name,
|
||||
description,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
tp.tenant_id,
|
||||
generate_gid(parse_tenant_id(tp.tenant_id), 30),
|
||||
s.organization_id,
|
||||
m.new_id,
|
||||
s.name,
|
||||
s.description,
|
||||
s.created_at,
|
||||
s.updated_at
|
||||
FROM _tp_copy_map m
|
||||
JOIN third_parties tp ON tp.id = m.old_id
|
||||
JOIN third_party_services s ON s.third_party_id = m.old_id;
|
||||
|
||||
INSERT INTO third_party_contacts (
|
||||
tenant_id,
|
||||
id,
|
||||
organization_id,
|
||||
third_party_id,
|
||||
full_name,
|
||||
email,
|
||||
phone,
|
||||
role,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
tp.tenant_id,
|
||||
generate_gid(parse_tenant_id(tp.tenant_id), 26),
|
||||
c.organization_id,
|
||||
m.new_id,
|
||||
c.full_name,
|
||||
c.email,
|
||||
c.phone,
|
||||
c.role,
|
||||
c.created_at,
|
||||
c.updated_at
|
||||
FROM _tp_copy_map m
|
||||
JOIN third_parties tp ON tp.id = m.old_id
|
||||
JOIN third_party_contacts c ON c.third_party_id = m.old_id;
|
||||
|
||||
DROP TABLE _tp_copy_map;
|
||||
DROP TABLE _tp_primary_parent;
|
||||
DROP TABLE third_party_third_parties;
|
||||
@@ -28,6 +28,11 @@ import (
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
// MaxThirdPartyLevel is the deepest sub-third-party nesting allowed. Level 1 is
|
||||
// a direct third party; each descendant adds one level, so the chain may not go
|
||||
// beyond level 4.
|
||||
const MaxThirdPartyLevel = 4
|
||||
|
||||
func (v ThirdParty) GetGeneratedDocumentID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
@@ -140,6 +145,7 @@ type (
|
||||
ThirdParty struct {
|
||||
ID gid.GID `db:"id"`
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
ParentThirdPartyID *gid.GID `db:"parent_third_party_id"`
|
||||
CommonThirdPartyID *gid.GID `db:"common_third_party_id"`
|
||||
Name string `db:"name"`
|
||||
Description *string `db:"description"`
|
||||
@@ -161,7 +167,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"`
|
||||
Level int `db:"level"`
|
||||
VettingStatus *ThirdPartyVettingStatus `db:"vetting_status"`
|
||||
VettingWebsiteURL *string `db:"vetting_website_url"`
|
||||
VettingProcedure *string `db:"vetting_procedure"`
|
||||
@@ -236,6 +242,7 @@ func (v *ThirdParty) LoadByID(
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_id,
|
||||
common_third_party_id,
|
||||
name,
|
||||
description,
|
||||
@@ -257,7 +264,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
@@ -308,6 +315,7 @@ func (v *ThirdParty) LoadByIDForUpdate(
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_id,
|
||||
common_third_party_id,
|
||||
name,
|
||||
description,
|
||||
@@ -329,7 +337,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
@@ -382,6 +390,7 @@ func (v *ThirdParty) LoadByNameAndOrganizationID(
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_id,
|
||||
common_third_party_id,
|
||||
name,
|
||||
description,
|
||||
@@ -403,7 +412,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
@@ -448,16 +457,18 @@ LIMIT 1;
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *ThirdParties) LoadByIDs(
|
||||
func (v *ThirdParty) LoadByNameAndParentThirdPartyID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
thirdPartyIDs []gid.GID,
|
||||
name string,
|
||||
parentThirdPartyID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_id,
|
||||
common_third_party_id,
|
||||
name,
|
||||
description,
|
||||
@@ -479,7 +490,84 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
vetting_processing_started_at,
|
||||
vetting_error_message,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
third_parties
|
||||
WHERE
|
||||
%s
|
||||
AND parent_third_party_id = @parent_third_party_id
|
||||
AND name = @name
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"parent_third_party_id": parentThirdPartyID,
|
||||
"name": name,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query thirdParty by name and parent: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
thirdParty, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ThirdParty])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect thirdParty: %w", err)
|
||||
}
|
||||
|
||||
*v = thirdParty
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *ThirdParties) LoadByIDs(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
thirdPartyIDs []gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_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,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
@@ -525,6 +613,7 @@ INSERT INTO
|
||||
tenant_id,
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_id,
|
||||
common_third_party_id,
|
||||
name,
|
||||
description,
|
||||
@@ -546,7 +635,7 @@ INSERT INTO
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
@@ -559,6 +648,7 @@ VALUES (
|
||||
@tenant_id,
|
||||
@third_party_id,
|
||||
@organization_id,
|
||||
@parent_third_party_id,
|
||||
@common_third_party_id,
|
||||
@name,
|
||||
@description,
|
||||
@@ -580,7 +670,7 @@ VALUES (
|
||||
@security_page_url,
|
||||
@trust_page_url,
|
||||
@show_on_trust_center,
|
||||
@first_level,
|
||||
@level,
|
||||
@vetting_status,
|
||||
@vetting_website_url,
|
||||
@vetting_procedure,
|
||||
@@ -595,6 +685,7 @@ VALUES (
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"third_party_id": v.ID,
|
||||
"organization_id": v.OrganizationID,
|
||||
"parent_third_party_id": v.ParentThirdPartyID,
|
||||
"common_third_party_id": v.CommonThirdPartyID,
|
||||
"name": v.Name,
|
||||
"description": v.Description,
|
||||
@@ -616,7 +707,7 @@ VALUES (
|
||||
"security_page_url": v.SecurityPageURL,
|
||||
"trust_page_url": v.TrustPageURL,
|
||||
"show_on_trust_center": v.ShowOnTrustCenter,
|
||||
"first_level": v.FirstLevel,
|
||||
"level": v.Level,
|
||||
"vetting_status": v.VettingStatus,
|
||||
"vetting_website_url": v.VettingWebsiteURL,
|
||||
"vetting_procedure": v.VettingProcedure,
|
||||
@@ -690,11 +781,13 @@ func (v *ThirdParties) LoadAllByOrganizationID(
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
filter *ThirdPartyFilter,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_id,
|
||||
common_third_party_id,
|
||||
name,
|
||||
description,
|
||||
@@ -716,7 +809,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
@@ -729,12 +822,14 @@ FROM
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND %s
|
||||
ORDER BY name ASC
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
@@ -763,6 +858,7 @@ func (v *ThirdParties) LoadByOrganizationID(
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_id,
|
||||
common_third_party_id,
|
||||
name,
|
||||
description,
|
||||
@@ -784,7 +880,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
@@ -834,6 +930,7 @@ SET
|
||||
name = @name,
|
||||
description = @description,
|
||||
category = @category,
|
||||
parent_third_party_id = @parent_third_party_id,
|
||||
headquarter_address = @headquarter_address,
|
||||
legal_name = @legal_name,
|
||||
website_url = @website_url,
|
||||
@@ -851,7 +948,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,
|
||||
level = @level,
|
||||
vetting_status = @vetting_status,
|
||||
vetting_website_url = @vetting_website_url,
|
||||
vetting_procedure = @vetting_procedure,
|
||||
@@ -870,6 +967,7 @@ WHERE %s
|
||||
"name": v.Name,
|
||||
"description": v.Description,
|
||||
"category": v.Category,
|
||||
"parent_third_party_id": v.ParentThirdPartyID,
|
||||
"headquarter_address": v.HeadquarterAddress,
|
||||
"legal_name": v.LegalName,
|
||||
"website_url": v.WebsiteURL,
|
||||
@@ -887,7 +985,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,
|
||||
"level": v.Level,
|
||||
"vetting_status": v.VettingStatus,
|
||||
"vetting_website_url": v.VettingWebsiteURL,
|
||||
"vetting_procedure": v.VettingProcedure,
|
||||
@@ -996,6 +1094,7 @@ WITH vend AS (
|
||||
v.id,
|
||||
v.tenant_id,
|
||||
v.organization_id,
|
||||
v.parent_third_party_id,
|
||||
v.common_third_party_id,
|
||||
v.name,
|
||||
v.description,
|
||||
@@ -1017,7 +1116,7 @@ WITH vend AS (
|
||||
v.security_page_url,
|
||||
v.trust_page_url,
|
||||
v.show_on_trust_center,
|
||||
v.first_level,
|
||||
v.level,
|
||||
v.vetting_status,
|
||||
v.vetting_website_url,
|
||||
v.vetting_procedure,
|
||||
@@ -1035,6 +1134,7 @@ WITH vend AS (
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_id,
|
||||
common_third_party_id,
|
||||
name,
|
||||
description,
|
||||
@@ -1056,7 +1156,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
@@ -1142,6 +1242,7 @@ WITH vend AS (
|
||||
v.id,
|
||||
v.tenant_id,
|
||||
v.organization_id,
|
||||
v.parent_third_party_id,
|
||||
v.common_third_party_id,
|
||||
v.name,
|
||||
v.description,
|
||||
@@ -1163,7 +1264,7 @@ WITH vend AS (
|
||||
v.security_page_url,
|
||||
v.trust_page_url,
|
||||
v.show_on_trust_center,
|
||||
v.first_level,
|
||||
v.level,
|
||||
v.vetting_status,
|
||||
v.vetting_website_url,
|
||||
v.vetting_procedure,
|
||||
@@ -1181,6 +1282,7 @@ WITH vend AS (
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_id,
|
||||
common_third_party_id,
|
||||
name,
|
||||
description,
|
||||
@@ -1202,7 +1304,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
@@ -1248,6 +1350,7 @@ WITH vend AS (
|
||||
v.id,
|
||||
v.tenant_id,
|
||||
v.organization_id,
|
||||
v.parent_third_party_id,
|
||||
v.common_third_party_id,
|
||||
v.name,
|
||||
v.description,
|
||||
@@ -1269,7 +1372,7 @@ WITH vend AS (
|
||||
v.security_page_url,
|
||||
v.trust_page_url,
|
||||
v.show_on_trust_center,
|
||||
v.first_level,
|
||||
v.level,
|
||||
v.vetting_status,
|
||||
v.vetting_website_url,
|
||||
v.vetting_procedure,
|
||||
@@ -1287,6 +1390,7 @@ WITH vend AS (
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_id,
|
||||
common_third_party_id,
|
||||
name,
|
||||
description,
|
||||
@@ -1308,7 +1412,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
@@ -1355,6 +1459,7 @@ WITH vend AS (
|
||||
v.id,
|
||||
v.tenant_id,
|
||||
v.organization_id,
|
||||
v.parent_third_party_id,
|
||||
v.common_third_party_id,
|
||||
v.name,
|
||||
v.description,
|
||||
@@ -1376,7 +1481,7 @@ WITH vend AS (
|
||||
v.security_page_url,
|
||||
v.trust_page_url,
|
||||
v.show_on_trust_center,
|
||||
v.first_level,
|
||||
v.level,
|
||||
v.vetting_status,
|
||||
v.vetting_website_url,
|
||||
v.vetting_procedure,
|
||||
@@ -1394,6 +1499,7 @@ WITH vend AS (
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_id,
|
||||
common_third_party_id,
|
||||
name,
|
||||
description,
|
||||
@@ -1415,7 +1521,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
@@ -1530,6 +1636,7 @@ WITH vend AS (
|
||||
v.id,
|
||||
v.tenant_id,
|
||||
v.organization_id,
|
||||
v.parent_third_party_id,
|
||||
v.common_third_party_id,
|
||||
v.name,
|
||||
v.description,
|
||||
@@ -1551,7 +1658,7 @@ WITH vend AS (
|
||||
v.security_page_url,
|
||||
v.trust_page_url,
|
||||
v.show_on_trust_center,
|
||||
v.first_level,
|
||||
v.level,
|
||||
v.vetting_status,
|
||||
v.vetting_website_url,
|
||||
v.vetting_procedure,
|
||||
@@ -1569,6 +1676,7 @@ WITH vend AS (
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_id,
|
||||
common_third_party_id,
|
||||
name,
|
||||
description,
|
||||
@@ -1590,7 +1698,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
@@ -1634,6 +1742,7 @@ func (v *ThirdParty) LoadByOrganizationIDAndCommonThirdPartyID(
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_id,
|
||||
common_third_party_id,
|
||||
name,
|
||||
description,
|
||||
@@ -1655,7 +1764,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
@@ -1754,6 +1863,7 @@ WITH tps AS (
|
||||
v.id,
|
||||
v.tenant_id,
|
||||
v.organization_id,
|
||||
v.parent_third_party_id,
|
||||
v.common_third_party_id,
|
||||
v.name,
|
||||
v.description,
|
||||
@@ -1775,7 +1885,7 @@ WITH tps AS (
|
||||
v.security_page_url,
|
||||
v.trust_page_url,
|
||||
v.show_on_trust_center,
|
||||
v.first_level,
|
||||
v.level,
|
||||
v.vetting_status,
|
||||
v.vetting_website_url,
|
||||
v.vetting_procedure,
|
||||
@@ -1793,6 +1903,7 @@ WITH tps AS (
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_id,
|
||||
common_third_party_id,
|
||||
name,
|
||||
description,
|
||||
@@ -1814,7 +1925,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
@@ -1847,3 +1958,254 @@ WHERE %s
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *ThirdParties) CountByParentThirdPartyID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
parentThirdPartyID gid.GID,
|
||||
) (int, error) {
|
||||
q := `
|
||||
SELECT
|
||||
COUNT(id)
|
||||
FROM
|
||||
third_parties
|
||||
WHERE
|
||||
%s
|
||||
AND parent_third_party_id = @parent_third_party_id
|
||||
`
|
||||
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) LoadAllAncestorsByThirdPartyID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
thirdPartyID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
WITH RECURSIVE ancestor_chain AS (
|
||||
SELECT
|
||||
tp.id,
|
||||
tp.tenant_id,
|
||||
tp.organization_id,
|
||||
tp.parent_third_party_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.level,
|
||||
tp.vetting_status,
|
||||
tp.vetting_website_url,
|
||||
tp.vetting_procedure,
|
||||
tp.vetting_processing_started_at,
|
||||
tp.vetting_error_message,
|
||||
tp.created_at,
|
||||
tp.updated_at,
|
||||
1 AS depth
|
||||
FROM third_parties tp
|
||||
WHERE %s
|
||||
AND tp.id = (
|
||||
SELECT parent_third_party_id
|
||||
FROM third_parties
|
||||
WHERE id = @third_party_id
|
||||
)
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
tp.id,
|
||||
tp.tenant_id,
|
||||
tp.organization_id,
|
||||
tp.parent_third_party_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.level,
|
||||
tp.vetting_status,
|
||||
tp.vetting_website_url,
|
||||
tp.vetting_procedure,
|
||||
tp.vetting_processing_started_at,
|
||||
tp.vetting_error_message,
|
||||
tp.created_at,
|
||||
tp.updated_at,
|
||||
ac.depth + 1
|
||||
FROM third_parties tp
|
||||
JOIN ancestor_chain ac ON tp.id = ac.parent_third_party_id
|
||||
WHERE ac.depth < @max_depth
|
||||
AND tp.tenant_id = ac.tenant_id
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_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,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
vetting_processing_started_at,
|
||||
vetting_error_message,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM ancestor_chain
|
||||
ORDER BY depth DESC
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"third_party_id": thirdPartyID,
|
||||
"max_depth": MaxThirdPartyLevel,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query ancestors: %w", err)
|
||||
}
|
||||
|
||||
ancestors, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ThirdParty])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect ancestors: %w", err)
|
||||
}
|
||||
|
||||
*v = ancestors
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *ThirdParties) LoadByParentThirdPartyID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
parentThirdPartyID gid.GID,
|
||||
cursor *page.Cursor[ThirdPartyOrderField],
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_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,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
vetting_processing_started_at,
|
||||
vetting_error_message,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
third_parties
|
||||
WHERE
|
||||
%s
|
||||
AND parent_third_party_id = @parent_third_party_id
|
||||
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
|
||||
}
|
||||
|
||||
@@ -21,15 +21,15 @@ import (
|
||||
type (
|
||||
ThirdPartyFilter struct {
|
||||
showOnTrustCenter *bool
|
||||
firstLevel *bool
|
||||
level *int
|
||||
query *string
|
||||
}
|
||||
)
|
||||
|
||||
func NewThirdPartyFilter(showOnTrustCenter *bool, firstLevel *bool, query *string) *ThirdPartyFilter {
|
||||
func NewThirdPartyFilter(showOnTrustCenter *bool, level *int, query *string) *ThirdPartyFilter {
|
||||
return &ThirdPartyFilter{
|
||||
showOnTrustCenter: showOnTrustCenter,
|
||||
firstLevel: firstLevel,
|
||||
level: level,
|
||||
query: query,
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ func (f *ThirdPartyFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||
args := pgx.StrictNamedArgs{
|
||||
"show_on_trust_center": nil,
|
||||
"filter_query": nil,
|
||||
"level": nil,
|
||||
}
|
||||
|
||||
if f.showOnTrustCenter != nil {
|
||||
@@ -48,10 +49,8 @@ func (f *ThirdPartyFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||
args["filter_query"] = *f.query
|
||||
}
|
||||
|
||||
if f.firstLevel != nil {
|
||||
args["first_level"] = *f.firstLevel
|
||||
} else {
|
||||
args["first_level"] = nil
|
||||
if f.level != nil {
|
||||
args["level"] = *f.level
|
||||
}
|
||||
|
||||
return args
|
||||
@@ -66,8 +65,8 @@ func (f *ThirdPartyFilter) SQLFragment() string {
|
||||
ELSE TRUE
|
||||
END
|
||||
AND CASE
|
||||
WHEN @first_level::boolean IS NOT NULL THEN
|
||||
first_level = @first_level::boolean
|
||||
WHEN @level::integer IS NOT NULL THEN
|
||||
level = @level::integer
|
||||
ELSE TRUE
|
||||
END
|
||||
AND CASE
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.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"`
|
||||
Purpose *string `db:"purpose"`
|
||||
}
|
||||
|
||||
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,
|
||||
purpose
|
||||
) VALUES (
|
||||
@parent_third_party_id,
|
||||
@child_third_party_id,
|
||||
@tenant_id,
|
||||
@created_at,
|
||||
@purpose
|
||||
)
|
||||
ON CONFLICT (parent_third_party_id, child_third_party_id) DO UPDATE SET
|
||||
purpose = COALESCE(EXCLUDED.purpose, third_party_third_parties.purpose)
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"parent_third_party_id": r.ParentThirdPartyID,
|
||||
"child_third_party_id": r.ChildThirdPartyID,
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"created_at": r.CreatedAt,
|
||||
"purpose": r.Purpose,
|
||||
}
|
||||
|
||||
_, 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.vetting_status,
|
||||
tp.vetting_website_url,
|
||||
tp.vetting_procedure,
|
||||
tp.vetting_processing_started_at,
|
||||
tp.vetting_error_message,
|
||||
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,
|
||||
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,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
vetting_processing_started_at,
|
||||
vetting_error_message,
|
||||
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
|
||||
}
|
||||
@@ -32,6 +32,7 @@ func (v *ThirdParty) LoadNextPendingVettingForUpdateSkipLocked(
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
parent_third_party_id,
|
||||
common_third_party_id,
|
||||
name,
|
||||
description,
|
||||
@@ -53,7 +54,7 @@ SELECT
|
||||
security_page_url,
|
||||
trust_page_url,
|
||||
show_on_trust_center,
|
||||
first_level,
|
||||
level,
|
||||
vetting_status,
|
||||
vetting_website_url,
|
||||
vetting_procedure,
|
||||
|
||||
Reference in New Issue
Block a user