From 7099a3d7022efdfaddbba13c76630f6b1a074f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Fri, 8 May 2026 19:08:58 +0400 Subject: [PATCH] Replace vendor JSON with common third parties API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CreateVendorDialog previously loaded the entire @probo/vendors JSON bundle client-side and used MiniSearch for fuzzy search. This replaces it with a GraphQL query against the common_third_parties database table, searched server-side via ILIKE filtering. Backend: adds CommonThirdParty GraphQL type, a pkg/thirdparty service, and a commonThirdParties(name) root query. Frontend: splits into CommonThirdPartyCombobox (display) and an @inline fragment read on selection via readInlineData. Signed-off-by: Émile Ré --- apps/console/src/hooks/useVendorSearch.ts | 47 ------- .../dialogs/CommonThirdPartyCombobox.tsx | 66 ++++++++++ .../vendors/dialogs/CreateVendorDialog.tsx | 118 ++++++++++++------ pkg/coredata/common_third_party.go | 12 +- pkg/coredata/common_third_party_filter.go | 45 +++++++ pkg/probod/probod.go | 4 + pkg/server/api/api.go | 3 + pkg/server/api/console/v1/base_resolvers.go | 16 +++ .../api/console/v1/graphql/base.graphql | 1 + .../v1/graphql/common_third_party.graphql | 34 +++++ pkg/server/api/console/v1/graphql_handler.go | 15 ++- pkg/server/api/console/v1/resolver.go | 4 + .../console/v1/types/common_third_party.go | 58 +++++++++ pkg/server/server.go | 3 + pkg/thirdparty/service.go | 49 ++++++++ 15 files changed, 388 insertions(+), 87 deletions(-) delete mode 100644 apps/console/src/hooks/useVendorSearch.ts create mode 100644 apps/console/src/pages/organizations/vendors/dialogs/CommonThirdPartyCombobox.tsx create mode 100644 pkg/coredata/common_third_party_filter.go create mode 100644 pkg/server/api/console/v1/graphql/common_third_party.graphql create mode 100644 pkg/server/api/console/v1/types/common_third_party.go create mode 100644 pkg/thirdparty/service.go diff --git a/apps/console/src/hooks/useVendorSearch.ts b/apps/console/src/hooks/useVendorSearch.ts deleted file mode 100644 index b45555604..000000000 --- a/apps/console/src/hooks/useVendorSearch.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2025-2026 Probo Inc . -// -// 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. - -import type { Vendor } from "@probo/vendors"; -import MiniSearch from "minisearch"; -import { useEffect, useRef, useState } from "react"; - -export function useVendorSearch() { - const searchRef = useRef<(s: string) => Vendor[]>(() => []); - const [query, setQuery] = useState(""); - const [vendors, setVendors] = useState([]); - useEffect(() => { - void import("@probo/vendors").then((module) => { - const ms = new MiniSearch({ - fields: ["name"], - storeFields: Object.keys(module.default[0]), - searchOptions: { - fuzzy: 0.1, - prefix: true, - }, - }); - ms.addAll(module.default.map(v => ({ ...v, id: v.name }))); - // @ts-expect-error not enough types to handle this case - searchRef.current = ms.search.bind(ms); - }); - }, []); - - return { - query, - vendors: vendors.slice(0, 20), - search: (s: string) => { - setQuery(s); - setVendors(searchRef.current(s)); - }, - }; -} diff --git a/apps/console/src/pages/organizations/vendors/dialogs/CommonThirdPartyCombobox.tsx b/apps/console/src/pages/organizations/vendors/dialogs/CommonThirdPartyCombobox.tsx new file mode 100644 index 000000000..19cf81997 --- /dev/null +++ b/apps/console/src/pages/organizations/vendors/dialogs/CommonThirdPartyCombobox.tsx @@ -0,0 +1,66 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import { faviconUrl } from "@probo/helpers"; +import { Avatar, ComboboxItem } from "@probo/ui"; +import type { PreloadedQuery } from "react-relay"; +import { graphql, usePreloadedQuery } from "react-relay"; + +import type { + CommonThirdPartyComboboxQuery$data, + CommonThirdPartyComboboxQuery, +} from "#/__generated__/core/CommonThirdPartyComboboxQuery.graphql"; + +export type CommonThirdPartyRef + = CommonThirdPartyComboboxQuery$data["commonThirdParties"][number]; + +export const commonThirdPartiesQuery = graphql` + query CommonThirdPartyComboboxQuery($name: String!) { + commonThirdParties(name: $name) { + id + name + websiteUrl + ...CreateVendorDialog_commonThirdParty + } + } +`; + +interface CommonThirdPartyComboboxProps { + queryRef: PreloadedQuery; + onSelect: (thirdPartyRef: CommonThirdPartyRef) => void; +} + +export function CommonThirdPartyCombobox({ + queryRef, + onSelect, +}: CommonThirdPartyComboboxProps) { + const data = usePreloadedQuery(commonThirdPartiesQuery, queryRef); + + return ( + <> + {data.commonThirdParties.map(thirdParty => ( + onSelect(thirdParty)} + > + + {thirdParty.name} + + ))} + + ); +} diff --git a/apps/console/src/pages/organizations/vendors/dialogs/CreateVendorDialog.tsx b/apps/console/src/pages/organizations/vendors/dialogs/CreateVendorDialog.tsx index 8a9e7788e..49e3c16d9 100644 --- a/apps/console/src/pages/organizations/vendors/dialogs/CreateVendorDialog.tsx +++ b/apps/console/src/pages/organizations/vendors/dialogs/CreateVendorDialog.tsx @@ -12,10 +12,8 @@ // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. -import { faviconUrl } from "@probo/helpers"; import { useTranslate } from "@probo/i18n"; import { - Avatar, Combobox, ComboboxItem, Dialog, @@ -24,11 +22,38 @@ import { IconPlusLarge, useDialogRef, } from "@probo/ui"; -import type { Vendor } from "@probo/vendors"; -import { type ReactNode } from "react"; +import { type ReactNode, Suspense, useState } from "react"; +import { useQueryLoader } from "react-relay"; +import { graphql, readInlineData } from "relay-runtime"; +import type { CommonThirdPartyComboboxQuery } from "#/__generated__/core/CommonThirdPartyComboboxQuery.graphql"; +import type { CreateVendorDialog_commonThirdParty$key } from "#/__generated__/core/CreateVendorDialog_commonThirdParty.graphql"; import { useCreateVendorMutation } from "#/hooks/graph/VendorGraph"; -import { useVendorSearch } from "#/hooks/useVendorSearch"; + +import { + commonThirdPartiesQuery, + CommonThirdPartyCombobox, + type CommonThirdPartyRef, +} from "./CommonThirdPartyCombobox"; + +const commonThirdPartyFragment = graphql` + fragment CreateVendorDialog_commonThirdParty on CommonThirdParty @inline { + name + description + category + websiteUrl + headquarterAddress + legalName + privacyPolicyUrl + serviceLevelAgreementUrl + dataProcessingAgreementUrl + certifications + securityPageUrl + trustPageUrl + statusPageUrl + termsOfServiceUrl + } +`; type Props = { children: ReactNode; @@ -42,35 +67,45 @@ export function CreateVendorDialog({ connection, }: Props) { const { __ } = useTranslate(); - const { search, vendors, query } = useVendorSearch(); const [createVendor] = useCreateVendorMutation(); + const dialogRef = useDialogRef(); + const [searchQuery, setSearchQuery] = useState(""); + const [queryRef, loadQuery] + = useQueryLoader(commonThirdPartiesQuery); - const onSelect = async (vendor: Vendor | string) => { + const onSelect = async (thirdPartyRef: CommonThirdPartyRef | string) => { const input - = typeof vendor === "string" + = typeof thirdPartyRef === "string" ? { organizationId, - name: vendor, + name: thirdPartyRef, category: null, } - : { - organizationId, - name: vendor.name, - description: vendor.description || null, - headquarterAddress: vendor.headquarterAddress || null, - legalName: vendor.legalName || null, - websiteUrl: vendor.websiteUrl || null, - category: vendor.category || null, - privacyPolicyUrl: vendor.privacyPolicyUrl || null, - serviceLevelAgreementUrl: vendor.serviceLevelAgreementUrl || null, - dataProcessingAgreementUrl: vendor.dataProcessingAgreementUrl || null, - certifications: vendor.certifications, - countries: vendor.countries, - securityPageUrl: vendor.securityPageUrl || null, - trustPageUrl: vendor.trustPageUrl || null, - statusPageUrl: vendor.statusPageUrl || null, - termsOfServiceUrl: vendor.termsOfServiceUrl || null, - }; + : (() => { + const tp = readInlineData( + commonThirdPartyFragment, + thirdPartyRef, + ); + return { + organizationId, + name: tp.name, + description: tp.description || null, + headquarterAddress: tp.headquarterAddress || null, + legalName: tp.legalName || null, + websiteUrl: tp.websiteUrl || null, + category: tp.category || null, + privacyPolicyUrl: tp.privacyPolicyUrl || null, + serviceLevelAgreementUrl: + tp.serviceLevelAgreementUrl || null, + dataProcessingAgreementUrl: + tp.dataProcessingAgreementUrl || null, + certifications: tp.certifications, + securityPageUrl: tp.securityPageUrl || null, + trustPageUrl: tp.trustPageUrl || null, + statusPageUrl: tp.statusPageUrl || null, + termsOfServiceUrl: tp.termsOfServiceUrl || null, + }; + })(); await createVendor({ variables: { input, @@ -82,25 +117,32 @@ export function CreateVendorDialog({ }); }; - const dialogRef = useDialogRef(); + const handleSearch = (name: string) => { + setSearchQuery(name); + if (name.trim().length >= 2) { + loadQuery({ name: name.trim() }); + } + }; return ( - - {vendors.map(vendor => ( - void onSelect(vendor)}> - - {vendor.name} - - ))} - {query.trim().length >= 2 && ( - void onSelect(query.trim())}> + + {queryRef && ( + + void onSelect(thirdPartyRef)} + /> + + )} + {searchQuery.trim().length >= 2 && ( + void onSelect(searchQuery.trim())}> {__("Create a new vendor")} {" "} : - {query} + {searchQuery} )} diff --git a/pkg/coredata/common_third_party.go b/pkg/coredata/common_third_party.go index 3dd460fbc..ee739e03b 100644 --- a/pkg/coredata/common_third_party.go +++ b/pkg/coredata/common_third_party.go @@ -18,6 +18,7 @@ import ( "context" "errors" "fmt" + "maps" "time" "github.com/jackc/pgx/v5" @@ -378,6 +379,7 @@ func (t CommonThirdParty) Delete( func (t *CommonThirdParties) LoadAll( ctx context.Context, conn pg.Querier, + filter *CommonThirdPartyFilter, ) error { q := ` SELECT @@ -403,10 +405,18 @@ SELECT updated_at FROM common_third_parties +WHERE + %s ORDER BY name ASC +LIMIT 20 ` - rows, err := conn.Query(ctx, q) + q = fmt.Sprintf(q, filter.SQLFragment()) + + args := pgx.StrictNamedArgs{} + maps.Copy(args, filter.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query common third parties: %w", err) } diff --git a/pkg/coredata/common_third_party_filter.go b/pkg/coredata/common_third_party_filter.go new file mode 100644 index 000000000..d8468cb75 --- /dev/null +++ b/pkg/coredata/common_third_party_filter.go @@ -0,0 +1,45 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 ( + "github.com/jackc/pgx/v5" +) + +type CommonThirdPartyFilter struct { + name *string +} + +func NewCommonThirdPartyFilter(name *string) *CommonThirdPartyFilter { + return &CommonThirdPartyFilter{name: name} +} + +func (f *CommonThirdPartyFilter) SQLFragment() string { + return `( + CASE + WHEN @filter_name::text IS NOT NULL THEN + name ILIKE '%' || @filter_name || '%' + ELSE TRUE + END +)` +} + +func (f *CommonThirdPartyFilter) SQLArguments() pgx.StrictNamedArgs { + args := pgx.StrictNamedArgs{"filter_name": nil} + if f.name != nil { + args["filter_name"] = *f.name + } + return args +} diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index 63074a0fe..4a1dc4e30 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -69,6 +69,7 @@ import ( "go.probo.inc/probo/pkg/server" "go.probo.inc/probo/pkg/server/trustedproxy" "go.probo.inc/probo/pkg/slack" + "go.probo.inc/probo/pkg/thirdparty" "go.probo.inc/probo/pkg/trust" "go.probo.inc/probo/pkg/webhook" "golang.org/x/sync/errgroup" @@ -519,6 +520,8 @@ func (impl *Implm) Run( l.Named("access-review"), ) + thirdPartyService := thirdparty.NewService(pgClient) + serverHandler, err := server.NewServer( server.Config{ AllowedOrigins: impl.cfg.Api.Cors.AllowedOrigins, @@ -532,6 +535,7 @@ func (impl *Implm) Run( Mailman: mailmanService, CookieBanner: cookieBannerService, Geoloc: geolocService, + ThirdParty: thirdPartyService, Slack: slackService, ConnectorRegistry: defaultConnectorRegistry, BaseURL: baseURL, diff --git a/pkg/server/api/api.go b/pkg/server/api/api.go index e5b127259..c2abacf91 100644 --- a/pkg/server/api/api.go +++ b/pkg/server/api/api.go @@ -44,6 +44,7 @@ import ( slack_v1 "go.probo.inc/probo/pkg/server/api/slack/v1" trust_v1 "go.probo.inc/probo/pkg/server/api/trust/v1" "go.probo.inc/probo/pkg/slack" + "go.probo.inc/probo/pkg/thirdparty" "go.probo.inc/probo/pkg/trust" ) @@ -61,6 +62,7 @@ type ( Mailman *mailman.Service CookieBanner *cookiebanner.Service Geoloc *geoloc.Service + ThirdParty *thirdparty.Service Cookie securecookie.Config TokenSecret string ConnectorRegistry *connector.ConnectorRegistry @@ -190,6 +192,7 @@ func NewServer(cfg Config) (*Server, error) { cfg.ConnectorRegistry, cfg.BaseURL, cfg.CustomDomainCname, + cfg.ThirdParty, ), cookieBannerHandler: cookiebanner_v1.NewMux( cfg.Logger.Named("cookiebanner.v1"), diff --git a/pkg/server/api/console/v1/base_resolvers.go b/pkg/server/api/console/v1/base_resolvers.go index 5a0e46f75..68ee26d17 100644 --- a/pkg/server/api/console/v1/base_resolvers.go +++ b/pkg/server/api/console/v1/base_resolvers.go @@ -408,6 +408,22 @@ func (r *queryResolver) Viewer(ctx context.Context) (*types.Viewer, error) { return &types.Viewer{ID: viewerID}, nil } +// CommonThirdParties is the resolver for the commonThirdParties field. +func (r *queryResolver) CommonThirdParties(ctx context.Context, name string) ([]*types.CommonThirdParty, error) { + parties, err := r.thirdParty.Search(ctx, name) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot search common third parties", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + result := make([]*types.CommonThirdParty, len(parties)) + for i, p := range parties { + result[i] = types.NewCommonThirdParty(p) + } + + return result, nil +} + // Mutation returns schema.MutationResolver implementation. func (r *Resolver) Mutation() schema.MutationResolver { return &mutationResolver{r} } diff --git a/pkg/server/api/console/v1/graphql/base.graphql b/pkg/server/api/console/v1/graphql/base.graphql index f6c91d07e..767ff0ff0 100644 --- a/pkg/server/api/console/v1/graphql/base.graphql +++ b/pkg/server/api/console/v1/graphql/base.graphql @@ -25,6 +25,7 @@ interface Node { type Query { node(id: ID!): Node! viewer: Viewer! + commonThirdParties(name: String!): [CommonThirdParty!]! } type Mutation diff --git a/pkg/server/api/console/v1/graphql/common_third_party.graphql b/pkg/server/api/console/v1/graphql/common_third_party.graphql new file mode 100644 index 000000000..e850d12ad --- /dev/null +++ b/pkg/server/api/console/v1/graphql/common_third_party.graphql @@ -0,0 +1,34 @@ +# Copyright (c) 2026 Probo Inc . +# +# 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. + +type CommonThirdParty + @goModel( + model: "go.probo.inc/probo/pkg/server/api/console/v1/types.CommonThirdParty" + ) { + id: ID! + name: String! + description: String + category: VendorCategory! + websiteUrl: String + headquarterAddress: String + legalName: String + privacyPolicyUrl: String + serviceLevelAgreementUrl: String + dataProcessingAgreementUrl: String + certifications: [String!]! + securityPageUrl: String + trustPageUrl: String + statusPageUrl: String + termsOfServiceUrl: String +} diff --git a/pkg/server/api/console/v1/graphql_handler.go b/pkg/server/api/console/v1/graphql_handler.go index ea5f9cb6b..ac117f0a6 100644 --- a/pkg/server/api/console/v1/graphql_handler.go +++ b/pkg/server/api/console/v1/graphql_handler.go @@ -28,9 +28,21 @@ import ( "go.probo.inc/probo/pkg/server/api/authz" "go.probo.inc/probo/pkg/server/api/console/v1/schema" "go.probo.inc/probo/pkg/server/gqlutils" + "go.probo.inc/probo/pkg/thirdparty" ) -func NewGraphQLHandler(iamSvc *iam.Service, proboSvc *probo.Service, esignSvc *esign.Service, accessReviewSvc *accessreview.Service, mailmanSvc *mailman.Service, cookieBannerSvc *cookiebanner.Service, connectorRegistry *connector.ConnectorRegistry, customDomainCname string, logger *log.Logger) http.Handler { +func NewGraphQLHandler( + iamSvc *iam.Service, + proboSvc *probo.Service, + esignSvc *esign.Service, + accessReviewSvc *accessreview.Service, + mailmanSvc *mailman.Service, + cookieBannerSvc *cookiebanner.Service, + connectorRegistry *connector.ConnectorRegistry, + customDomainCname string, + logger *log.Logger, + thirdPartySvc *thirdparty.Service, +) http.Handler { config := schema.Config{ Resolvers: &Resolver{ authorize: authz.NewAuthorizeFunc(iamSvc, logger), @@ -41,6 +53,7 @@ func NewGraphQLHandler(iamSvc *iam.Service, proboSvc *probo.Service, esignSvc *e mailman: mailmanSvc, cookieBanner: cookieBannerSvc, connectorRegistry: connectorRegistry, + thirdParty: thirdPartySvc, customDomainCname: customDomainCname, logger: logger, }, diff --git a/pkg/server/api/console/v1/resolver.go b/pkg/server/api/console/v1/resolver.go index 9aa562055..3fd91eff4 100644 --- a/pkg/server/api/console/v1/resolver.go +++ b/pkg/server/api/console/v1/resolver.go @@ -41,6 +41,7 @@ import ( "go.probo.inc/probo/pkg/server/api/authz" "go.probo.inc/probo/pkg/server/api/console/v1/dataloader" "go.probo.inc/probo/pkg/server/api/console/v1/types" + "go.probo.inc/probo/pkg/thirdparty" ) type ( @@ -53,6 +54,7 @@ type ( mailman *mailman.Service cookieBanner *cookiebanner.Service connectorRegistry *connector.ConnectorRegistry + thirdParty *thirdparty.Service logger *log.Logger customDomainCname string } @@ -71,6 +73,7 @@ func NewMux( connectorRegistry *connector.ConnectorRegistry, baseURL *baseurl.BaseURL, customDomainCname string, + thirdPartySvc *thirdparty.Service, ) *chi.Mux { r := chi.NewMux() @@ -86,6 +89,7 @@ func NewMux( connectorRegistry, customDomainCname, logger, + thirdPartySvc, ) r.Group(func(r chi.Router) { diff --git a/pkg/server/api/console/v1/types/common_third_party.go b/pkg/server/api/console/v1/types/common_third_party.go new file mode 100644 index 000000000..3a345899a --- /dev/null +++ b/pkg/server/api/console/v1/types/common_third_party.go @@ -0,0 +1,58 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 types + +import ( + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" +) + +type CommonThirdParty struct { + ID gid.GID `json:"id"` + Name string `json:"name"` + Description *string `json:"description,omitempty"` + Category coredata.VendorCategory `json:"category"` + WebsiteURL *string `json:"websiteUrl,omitempty"` + HeadquarterAddress *string `json:"headquarterAddress,omitempty"` + LegalName *string `json:"legalName,omitempty"` + PrivacyPolicyURL *string `json:"privacyPolicyUrl,omitempty"` + ServiceLevelAgreementURL *string `json:"serviceLevelAgreementUrl,omitempty"` + DataProcessingAgreementURL *string `json:"dataProcessingAgreementUrl,omitempty"` + Certifications []string `json:"certifications"` + SecurityPageURL *string `json:"securityPageUrl,omitempty"` + TrustPageURL *string `json:"trustPageUrl,omitempty"` + StatusPageURL *string `json:"statusPageUrl,omitempty"` + TermsOfServiceURL *string `json:"termsOfServiceUrl,omitempty"` +} + +func NewCommonThirdParty(c *coredata.CommonThirdParty) *CommonThirdParty { + return &CommonThirdParty{ + ID: c.ID, + Name: c.Name, + Description: c.Description, + Category: c.Category, + WebsiteURL: c.WebsiteURL, + HeadquarterAddress: c.HeadquarterAddress, + LegalName: c.LegalName, + PrivacyPolicyURL: c.PrivacyPolicyURL, + ServiceLevelAgreementURL: c.ServiceLevelAgreementURL, + DataProcessingAgreementURL: c.DataProcessingAgreementURL, + Certifications: c.Certifications, + SecurityPageURL: c.SecurityPageURL, + TrustPageURL: c.TrustPageURL, + StatusPageURL: c.StatusPageURL, + TermsOfServiceURL: c.TermsOfServiceURL, + } +} diff --git a/pkg/server/server.go b/pkg/server/server.go index b3ee1ed8f..078dc18f3 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -42,6 +42,7 @@ import ( trust_web "go.probo.inc/probo/pkg/server/trust" console_web "go.probo.inc/probo/pkg/server/web" "go.probo.inc/probo/pkg/slack" + "go.probo.inc/probo/pkg/thirdparty" "go.probo.inc/probo/pkg/trust" "go.probo.inc/probo/pkg/uri" ) @@ -60,6 +61,7 @@ type Config struct { Mailman *mailman.Service CookieBanner *cookiebanner.Service Geoloc *geoloc.Service + ThirdParty *thirdparty.Service Cookie securecookie.Config TokenSecret string ConnectorRegistry *connector.ConnectorRegistry @@ -95,6 +97,7 @@ func NewServer(cfg Config) (*Server, error) { Mailman: cfg.Mailman, CookieBanner: cfg.CookieBanner, Geoloc: cfg.Geoloc, + ThirdParty: cfg.ThirdParty, Cookie: cfg.Cookie, TokenSecret: cfg.TokenSecret, ConnectorRegistry: cfg.ConnectorRegistry, diff --git a/pkg/thirdparty/service.go b/pkg/thirdparty/service.go new file mode 100644 index 000000000..1bc721052 --- /dev/null +++ b/pkg/thirdparty/service.go @@ -0,0 +1,49 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 thirdparty + +import ( + "context" + "fmt" + + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/coredata" +) + +type Service struct { + pg *pg.Client +} + +func NewService(pgClient *pg.Client) *Service { + return &Service{pg: pgClient} +} + +func (s *Service) Search(ctx context.Context, name string) ([]*coredata.CommonThirdParty, error) { + var parties coredata.CommonThirdParties + + filter := coredata.NewCommonThirdPartyFilter(&name) + + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + return parties.LoadAll(ctx, conn, filter) + }, + ) + if err != nil { + return nil, fmt.Errorf("cannot search common third parties: %w", err) + } + + return parties, nil +}