Replace vendor JSON with common third parties API
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é <emile@getprobo.com>
This commit is contained in:
@@ -1,47 +0,0 @@
|
|||||||
// 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.
|
|
||||||
|
|
||||||
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<Vendor[]>([]);
|
|
||||||
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));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
66
apps/console/src/pages/organizations/vendors/dialogs/CommonThirdPartyCombobox.tsx
vendored
Normal file
66
apps/console/src/pages/organizations/vendors/dialogs/CommonThirdPartyCombobox.tsx
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
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<CommonThirdPartyComboboxQuery>;
|
||||||
|
onSelect: (thirdPartyRef: CommonThirdPartyRef) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommonThirdPartyCombobox({
|
||||||
|
queryRef,
|
||||||
|
onSelect,
|
||||||
|
}: CommonThirdPartyComboboxProps) {
|
||||||
|
const data = usePreloadedQuery(commonThirdPartiesQuery, queryRef);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{data.commonThirdParties.map(thirdParty => (
|
||||||
|
<ComboboxItem
|
||||||
|
key={thirdParty.id}
|
||||||
|
onClick={() => onSelect(thirdParty)}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
name={thirdParty.name}
|
||||||
|
src={faviconUrl(thirdParty.websiteUrl)}
|
||||||
|
/>
|
||||||
|
{thirdParty.name}
|
||||||
|
</ComboboxItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,10 +12,8 @@
|
|||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
import { faviconUrl } from "@probo/helpers";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
|
||||||
Combobox,
|
Combobox,
|
||||||
ComboboxItem,
|
ComboboxItem,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -24,11 +22,38 @@ import {
|
|||||||
IconPlusLarge,
|
IconPlusLarge,
|
||||||
useDialogRef,
|
useDialogRef,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import type { Vendor } from "@probo/vendors";
|
import { type ReactNode, Suspense, useState } from "react";
|
||||||
import { type ReactNode } 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 { 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 = {
|
type Props = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -42,35 +67,45 @@ export function CreateVendorDialog({
|
|||||||
connection,
|
connection,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { search, vendors, query } = useVendorSearch();
|
|
||||||
const [createVendor] = useCreateVendorMutation();
|
const [createVendor] = useCreateVendorMutation();
|
||||||
|
const dialogRef = useDialogRef();
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [queryRef, loadQuery]
|
||||||
|
= useQueryLoader<CommonThirdPartyComboboxQuery>(commonThirdPartiesQuery);
|
||||||
|
|
||||||
const onSelect = async (vendor: Vendor | string) => {
|
const onSelect = async (thirdPartyRef: CommonThirdPartyRef | string) => {
|
||||||
const input
|
const input
|
||||||
= typeof vendor === "string"
|
= typeof thirdPartyRef === "string"
|
||||||
? {
|
? {
|
||||||
organizationId,
|
organizationId,
|
||||||
name: vendor,
|
name: thirdPartyRef,
|
||||||
category: null,
|
category: null,
|
||||||
}
|
}
|
||||||
: {
|
: (() => {
|
||||||
|
const tp = readInlineData<CreateVendorDialog_commonThirdParty$key>(
|
||||||
|
commonThirdPartyFragment,
|
||||||
|
thirdPartyRef,
|
||||||
|
);
|
||||||
|
return {
|
||||||
organizationId,
|
organizationId,
|
||||||
name: vendor.name,
|
name: tp.name,
|
||||||
description: vendor.description || null,
|
description: tp.description || null,
|
||||||
headquarterAddress: vendor.headquarterAddress || null,
|
headquarterAddress: tp.headquarterAddress || null,
|
||||||
legalName: vendor.legalName || null,
|
legalName: tp.legalName || null,
|
||||||
websiteUrl: vendor.websiteUrl || null,
|
websiteUrl: tp.websiteUrl || null,
|
||||||
category: vendor.category || null,
|
category: tp.category || null,
|
||||||
privacyPolicyUrl: vendor.privacyPolicyUrl || null,
|
privacyPolicyUrl: tp.privacyPolicyUrl || null,
|
||||||
serviceLevelAgreementUrl: vendor.serviceLevelAgreementUrl || null,
|
serviceLevelAgreementUrl:
|
||||||
dataProcessingAgreementUrl: vendor.dataProcessingAgreementUrl || null,
|
tp.serviceLevelAgreementUrl || null,
|
||||||
certifications: vendor.certifications,
|
dataProcessingAgreementUrl:
|
||||||
countries: vendor.countries,
|
tp.dataProcessingAgreementUrl || null,
|
||||||
securityPageUrl: vendor.securityPageUrl || null,
|
certifications: tp.certifications,
|
||||||
trustPageUrl: vendor.trustPageUrl || null,
|
securityPageUrl: tp.securityPageUrl || null,
|
||||||
statusPageUrl: vendor.statusPageUrl || null,
|
trustPageUrl: tp.trustPageUrl || null,
|
||||||
termsOfServiceUrl: vendor.termsOfServiceUrl || null,
|
statusPageUrl: tp.statusPageUrl || null,
|
||||||
|
termsOfServiceUrl: tp.termsOfServiceUrl || null,
|
||||||
};
|
};
|
||||||
|
})();
|
||||||
await createVendor({
|
await createVendor({
|
||||||
variables: {
|
variables: {
|
||||||
input,
|
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 (
|
return (
|
||||||
<Dialog ref={dialogRef} trigger={children} title={__("Add a vendor")}>
|
<Dialog ref={dialogRef} trigger={children} title={__("Add a vendor")}>
|
||||||
<DialogContent className="p-6">
|
<DialogContent className="p-6">
|
||||||
<Combobox onSearch={search} placeholder={__("Type vendor's name")}>
|
<Combobox onSearch={handleSearch} placeholder={__("Type vendor's name")}>
|
||||||
{vendors.map(vendor => (
|
{queryRef && (
|
||||||
<ComboboxItem key={vendor.name} onClick={() => void onSelect(vendor)}>
|
<Suspense>
|
||||||
<Avatar name={vendor.name} src={faviconUrl(vendor.websiteUrl)} />
|
<CommonThirdPartyCombobox
|
||||||
{vendor.name}
|
queryRef={queryRef}
|
||||||
</ComboboxItem>
|
onSelect={thirdPartyRef => void onSelect(thirdPartyRef)}
|
||||||
))}
|
/>
|
||||||
{query.trim().length >= 2 && (
|
</Suspense>
|
||||||
<ComboboxItem onClick={() => void onSelect(query.trim())}>
|
)}
|
||||||
|
{searchQuery.trim().length >= 2 && (
|
||||||
|
<ComboboxItem onClick={() => void onSelect(searchQuery.trim())}>
|
||||||
<IconPlusLarge size={20} />
|
<IconPlusLarge size={20} />
|
||||||
{__("Create a new vendor")}
|
{__("Create a new vendor")}
|
||||||
{" "}
|
{" "}
|
||||||
:
|
:
|
||||||
{query}
|
{searchQuery}
|
||||||
</ComboboxItem>
|
</ComboboxItem>
|
||||||
)}
|
)}
|
||||||
</Combobox>
|
</Combobox>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
@@ -378,6 +379,7 @@ func (t CommonThirdParty) Delete(
|
|||||||
func (t *CommonThirdParties) LoadAll(
|
func (t *CommonThirdParties) LoadAll(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Querier,
|
conn pg.Querier,
|
||||||
|
filter *CommonThirdPartyFilter,
|
||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
@@ -403,10 +405,18 @@ SELECT
|
|||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
common_third_parties
|
common_third_parties
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
ORDER BY name ASC
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot query common third parties: %w", err)
|
return fmt.Errorf("cannot query common third parties: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
45
pkg/coredata/common_third_party_filter.go
Normal file
45
pkg/coredata/common_third_party_filter.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// 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 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
|
||||||
|
}
|
||||||
@@ -69,6 +69,7 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/server"
|
"go.probo.inc/probo/pkg/server"
|
||||||
"go.probo.inc/probo/pkg/server/trustedproxy"
|
"go.probo.inc/probo/pkg/server/trustedproxy"
|
||||||
"go.probo.inc/probo/pkg/slack"
|
"go.probo.inc/probo/pkg/slack"
|
||||||
|
"go.probo.inc/probo/pkg/thirdparty"
|
||||||
"go.probo.inc/probo/pkg/trust"
|
"go.probo.inc/probo/pkg/trust"
|
||||||
"go.probo.inc/probo/pkg/webhook"
|
"go.probo.inc/probo/pkg/webhook"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
@@ -519,6 +520,8 @@ func (impl *Implm) Run(
|
|||||||
l.Named("access-review"),
|
l.Named("access-review"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
thirdPartyService := thirdparty.NewService(pgClient)
|
||||||
|
|
||||||
serverHandler, err := server.NewServer(
|
serverHandler, err := server.NewServer(
|
||||||
server.Config{
|
server.Config{
|
||||||
AllowedOrigins: impl.cfg.Api.Cors.AllowedOrigins,
|
AllowedOrigins: impl.cfg.Api.Cors.AllowedOrigins,
|
||||||
@@ -532,6 +535,7 @@ func (impl *Implm) Run(
|
|||||||
Mailman: mailmanService,
|
Mailman: mailmanService,
|
||||||
CookieBanner: cookieBannerService,
|
CookieBanner: cookieBannerService,
|
||||||
Geoloc: geolocService,
|
Geoloc: geolocService,
|
||||||
|
ThirdParty: thirdPartyService,
|
||||||
Slack: slackService,
|
Slack: slackService,
|
||||||
ConnectorRegistry: defaultConnectorRegistry,
|
ConnectorRegistry: defaultConnectorRegistry,
|
||||||
BaseURL: baseURL,
|
BaseURL: baseURL,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import (
|
|||||||
slack_v1 "go.probo.inc/probo/pkg/server/api/slack/v1"
|
slack_v1 "go.probo.inc/probo/pkg/server/api/slack/v1"
|
||||||
trust_v1 "go.probo.inc/probo/pkg/server/api/trust/v1"
|
trust_v1 "go.probo.inc/probo/pkg/server/api/trust/v1"
|
||||||
"go.probo.inc/probo/pkg/slack"
|
"go.probo.inc/probo/pkg/slack"
|
||||||
|
"go.probo.inc/probo/pkg/thirdparty"
|
||||||
"go.probo.inc/probo/pkg/trust"
|
"go.probo.inc/probo/pkg/trust"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ type (
|
|||||||
Mailman *mailman.Service
|
Mailman *mailman.Service
|
||||||
CookieBanner *cookiebanner.Service
|
CookieBanner *cookiebanner.Service
|
||||||
Geoloc *geoloc.Service
|
Geoloc *geoloc.Service
|
||||||
|
ThirdParty *thirdparty.Service
|
||||||
Cookie securecookie.Config
|
Cookie securecookie.Config
|
||||||
TokenSecret string
|
TokenSecret string
|
||||||
ConnectorRegistry *connector.ConnectorRegistry
|
ConnectorRegistry *connector.ConnectorRegistry
|
||||||
@@ -190,6 +192,7 @@ func NewServer(cfg Config) (*Server, error) {
|
|||||||
cfg.ConnectorRegistry,
|
cfg.ConnectorRegistry,
|
||||||
cfg.BaseURL,
|
cfg.BaseURL,
|
||||||
cfg.CustomDomainCname,
|
cfg.CustomDomainCname,
|
||||||
|
cfg.ThirdParty,
|
||||||
),
|
),
|
||||||
cookieBannerHandler: cookiebanner_v1.NewMux(
|
cookieBannerHandler: cookiebanner_v1.NewMux(
|
||||||
cfg.Logger.Named("cookiebanner.v1"),
|
cfg.Logger.Named("cookiebanner.v1"),
|
||||||
|
|||||||
@@ -408,6 +408,22 @@ func (r *queryResolver) Viewer(ctx context.Context) (*types.Viewer, error) {
|
|||||||
return &types.Viewer{ID: viewerID}, nil
|
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.
|
// Mutation returns schema.MutationResolver implementation.
|
||||||
func (r *Resolver) Mutation() schema.MutationResolver { return &mutationResolver{r} }
|
func (r *Resolver) Mutation() schema.MutationResolver { return &mutationResolver{r} }
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ interface Node {
|
|||||||
type Query {
|
type Query {
|
||||||
node(id: ID!): Node!
|
node(id: ID!): Node!
|
||||||
viewer: Viewer!
|
viewer: Viewer!
|
||||||
|
commonThirdParties(name: String!): [CommonThirdParty!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation
|
type Mutation
|
||||||
|
|||||||
34
pkg/server/api/console/v1/graphql/common_third_party.graphql
Normal file
34
pkg/server/api/console/v1/graphql/common_third_party.graphql
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -28,9 +28,21 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/server/api/authz"
|
"go.probo.inc/probo/pkg/server/api/authz"
|
||||||
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
|
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
|
||||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
"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{
|
config := schema.Config{
|
||||||
Resolvers: &Resolver{
|
Resolvers: &Resolver{
|
||||||
authorize: authz.NewAuthorizeFunc(iamSvc, logger),
|
authorize: authz.NewAuthorizeFunc(iamSvc, logger),
|
||||||
@@ -41,6 +53,7 @@ func NewGraphQLHandler(iamSvc *iam.Service, proboSvc *probo.Service, esignSvc *e
|
|||||||
mailman: mailmanSvc,
|
mailman: mailmanSvc,
|
||||||
cookieBanner: cookieBannerSvc,
|
cookieBanner: cookieBannerSvc,
|
||||||
connectorRegistry: connectorRegistry,
|
connectorRegistry: connectorRegistry,
|
||||||
|
thirdParty: thirdPartySvc,
|
||||||
customDomainCname: customDomainCname,
|
customDomainCname: customDomainCname,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/server/api/authz"
|
"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/dataloader"
|
||||||
"go.probo.inc/probo/pkg/server/api/console/v1/types"
|
"go.probo.inc/probo/pkg/server/api/console/v1/types"
|
||||||
|
"go.probo.inc/probo/pkg/thirdparty"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -53,6 +54,7 @@ type (
|
|||||||
mailman *mailman.Service
|
mailman *mailman.Service
|
||||||
cookieBanner *cookiebanner.Service
|
cookieBanner *cookiebanner.Service
|
||||||
connectorRegistry *connector.ConnectorRegistry
|
connectorRegistry *connector.ConnectorRegistry
|
||||||
|
thirdParty *thirdparty.Service
|
||||||
logger *log.Logger
|
logger *log.Logger
|
||||||
customDomainCname string
|
customDomainCname string
|
||||||
}
|
}
|
||||||
@@ -71,6 +73,7 @@ func NewMux(
|
|||||||
connectorRegistry *connector.ConnectorRegistry,
|
connectorRegistry *connector.ConnectorRegistry,
|
||||||
baseURL *baseurl.BaseURL,
|
baseURL *baseurl.BaseURL,
|
||||||
customDomainCname string,
|
customDomainCname string,
|
||||||
|
thirdPartySvc *thirdparty.Service,
|
||||||
) *chi.Mux {
|
) *chi.Mux {
|
||||||
r := chi.NewMux()
|
r := chi.NewMux()
|
||||||
|
|
||||||
@@ -86,6 +89,7 @@ func NewMux(
|
|||||||
connectorRegistry,
|
connectorRegistry,
|
||||||
customDomainCname,
|
customDomainCname,
|
||||||
logger,
|
logger,
|
||||||
|
thirdPartySvc,
|
||||||
)
|
)
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
|
|||||||
58
pkg/server/api/console/v1/types/common_third_party.go
Normal file
58
pkg/server/api/console/v1/types/common_third_party.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// 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 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ import (
|
|||||||
trust_web "go.probo.inc/probo/pkg/server/trust"
|
trust_web "go.probo.inc/probo/pkg/server/trust"
|
||||||
console_web "go.probo.inc/probo/pkg/server/web"
|
console_web "go.probo.inc/probo/pkg/server/web"
|
||||||
"go.probo.inc/probo/pkg/slack"
|
"go.probo.inc/probo/pkg/slack"
|
||||||
|
"go.probo.inc/probo/pkg/thirdparty"
|
||||||
"go.probo.inc/probo/pkg/trust"
|
"go.probo.inc/probo/pkg/trust"
|
||||||
"go.probo.inc/probo/pkg/uri"
|
"go.probo.inc/probo/pkg/uri"
|
||||||
)
|
)
|
||||||
@@ -60,6 +61,7 @@ type Config struct {
|
|||||||
Mailman *mailman.Service
|
Mailman *mailman.Service
|
||||||
CookieBanner *cookiebanner.Service
|
CookieBanner *cookiebanner.Service
|
||||||
Geoloc *geoloc.Service
|
Geoloc *geoloc.Service
|
||||||
|
ThirdParty *thirdparty.Service
|
||||||
Cookie securecookie.Config
|
Cookie securecookie.Config
|
||||||
TokenSecret string
|
TokenSecret string
|
||||||
ConnectorRegistry *connector.ConnectorRegistry
|
ConnectorRegistry *connector.ConnectorRegistry
|
||||||
@@ -95,6 +97,7 @@ func NewServer(cfg Config) (*Server, error) {
|
|||||||
Mailman: cfg.Mailman,
|
Mailman: cfg.Mailman,
|
||||||
CookieBanner: cfg.CookieBanner,
|
CookieBanner: cfg.CookieBanner,
|
||||||
Geoloc: cfg.Geoloc,
|
Geoloc: cfg.Geoloc,
|
||||||
|
ThirdParty: cfg.ThirdParty,
|
||||||
Cookie: cfg.Cookie,
|
Cookie: cfg.Cookie,
|
||||||
TokenSecret: cfg.TokenSecret,
|
TokenSecret: cfg.TokenSecret,
|
||||||
ConnectorRegistry: cfg.ConnectorRegistry,
|
ConnectorRegistry: cfg.ConnectorRegistry,
|
||||||
|
|||||||
49
pkg/thirdparty/service.go
vendored
Normal file
49
pkg/thirdparty/service.go
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// 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 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user