diff --git a/ui/actions/findings/findings.ts b/ui/actions/findings/findings.ts index d226795483..7ce7f931ad 100644 --- a/ui/actions/findings/findings.ts +++ b/ui/actions/findings/findings.ts @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; export const getFindings = async ({ page = 1, @@ -24,12 +25,7 @@ export const getFindings = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - // Skip filter[search] since it's already added via the `query` param above - if (key !== "filter[search]") { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const findings = await fetch(url.toString(), { @@ -65,12 +61,7 @@ export const getLatestFindings = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - // Skip filter[search] since it's already added via the `query` param above - if (key !== "filter[search]") { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const findings = await fetch(url.toString(), { @@ -96,20 +87,13 @@ export const getMetadataInfo = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - // Define filters to exclude - const excludedFilters = [ + appendSanitizedProviderTypeFilters(url, filters, { + excludedKeyIncludes: [ "region__in", "service__in", "resource_type__in", "resource_groups__in", - ]; - if ( - key !== "filter[search]" && - !excludedFilters.some((filter) => key.includes(filter)) - ) { - url.searchParams.append(key, String(value)); - } + ], }); try { @@ -136,20 +120,13 @@ export const getLatestMetadataInfo = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - // Define filters to exclude - const excludedFilters = [ + appendSanitizedProviderTypeFilters(url, filters, { + excludedKeyIncludes: [ "region__in", "service__in", "resource_type__in", "resource_groups__in", - ]; - if ( - key !== "filter[search]" && - !excludedFilters.some((filter) => key.includes(filter)) - ) { - url.searchParams.append(key, String(value)); - } + ], }); try { diff --git a/ui/actions/overview/attack-surface/attack-surface.ts b/ui/actions/overview/attack-surface/attack-surface.ts index 342e958bc3..4a97a406d4 100644 --- a/ui/actions/overview/attack-surface/attack-surface.ts +++ b/ui/actions/overview/attack-surface/attack-surface.ts @@ -1,6 +1,7 @@ "use server"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { AttackSurfaceOverviewResponse } from "./types"; @@ -14,12 +15,7 @@ export const getAttackSurfaceOverview = async ({ const url = new URL(`${apiBaseUrl}/overviews/attack-surfaces`); - // Handle multiple filters - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]" && value !== undefined) { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/overview/compliance-watchlist/compliance-watchlist.ts b/ui/actions/overview/compliance-watchlist/compliance-watchlist.ts index 647ee99e45..026a1b426b 100644 --- a/ui/actions/overview/compliance-watchlist/compliance-watchlist.ts +++ b/ui/actions/overview/compliance-watchlist/compliance-watchlist.ts @@ -1,6 +1,7 @@ "use server"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { ComplianceWatchlistResponse } from "./compliance-watchlist.types"; @@ -15,11 +16,7 @@ export const getComplianceWatchlist = async ({ // Append filter parameters (provider_id, provider_type, etc.) // Exclude filter[search] as this endpoint doesn't support text search - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]" && value !== undefined) { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { headers }); diff --git a/ui/actions/overview/findings/findings.ts b/ui/actions/overview/findings/findings.ts index 28dbbfbe8d..2aa771fb26 100644 --- a/ui/actions/overview/findings/findings.ts +++ b/ui/actions/overview/findings/findings.ts @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { FindingsSeverityOverviewResponse } from "./types"; @@ -28,17 +29,8 @@ export const getFindingsByStatus = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - // Handle multiple filters, but exclude muted filter as overviews endpoint doesn't support it - Object.entries(filters).forEach(([key, value]) => { - // The overviews/findings endpoint does not support status or muted filters - // (allowed filters include date, region, provider fields). Exclude unsupported ones. - if ( - key !== "filter[search]" && - key !== "filter[muted]" && - key !== "filter[status]" - ) { - url.searchParams.append(key, String(value)); - } + appendSanitizedProviderTypeFilters(url, filters, { + excludedKeys: ["filter[search]", "filter[muted]", "filter[status]"], }); try { @@ -62,15 +54,8 @@ export const getFindingsBySeverity = async ({ const url = new URL(`${apiBaseUrl}/overviews/findings_severity`); - // Handle multiple filters, but exclude unsupported filters - Object.entries(filters).forEach(([key, value]) => { - if ( - key !== "filter[search]" && - key !== "filter[muted]" && - value !== undefined - ) { - url.searchParams.append(key, String(value)); - } + appendSanitizedProviderTypeFilters(url, filters, { + excludedKeys: ["filter[search]", "filter[muted]"], }); try { diff --git a/ui/actions/overview/providers/providers.ts b/ui/actions/overview/providers/providers.ts index 5222856ff5..9aaec5df8c 100644 --- a/ui/actions/overview/providers/providers.ts +++ b/ui/actions/overview/providers/providers.ts @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { ProvidersOverviewResponse } from "./types"; @@ -28,11 +29,7 @@ export const getProvidersOverview = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]" && value !== undefined) { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/overview/regions/regions.ts b/ui/actions/overview/regions/regions.ts index f32632208c..31610892ef 100644 --- a/ui/actions/overview/regions/regions.ts +++ b/ui/actions/overview/regions/regions.ts @@ -1,6 +1,7 @@ "use server"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { RegionsOverviewResponse } from "./types"; @@ -14,12 +15,7 @@ export const getRegionsOverview = async ({ const url = new URL(`${apiBaseUrl}/overviews/regions`); - // Handle multiple filters - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]" && value !== undefined) { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/overview/resources-inventory/resources-inventory.ts b/ui/actions/overview/resources-inventory/resources-inventory.ts index 7cd353df39..264da6e375 100644 --- a/ui/actions/overview/resources-inventory/resources-inventory.ts +++ b/ui/actions/overview/resources-inventory/resources-inventory.ts @@ -1,6 +1,7 @@ "use server"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { ResourceGroupOverviewResponse } from "./types"; @@ -14,12 +15,7 @@ export const getResourceGroupOverview = async ({ const url = new URL(`${apiBaseUrl}/overviews/resource-groups`); - // Handle multiple filters - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]" && value !== undefined) { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/overview/risk-radar/risk-radar.ts b/ui/actions/overview/risk-radar/risk-radar.ts index 44c04ad87e..419330dc18 100644 --- a/ui/actions/overview/risk-radar/risk-radar.ts +++ b/ui/actions/overview/risk-radar/risk-radar.ts @@ -1,6 +1,7 @@ "use server"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { CategoryOverviewResponse } from "./types"; @@ -14,12 +15,7 @@ export const getCategoryOverview = async ({ const url = new URL(`${apiBaseUrl}/overviews/categories`); - // Handle multiple filters - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]" && value !== undefined) { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/overview/services/services.ts b/ui/actions/overview/services/services.ts index 3e73776dd2..1e55c04531 100644 --- a/ui/actions/overview/services/services.ts +++ b/ui/actions/overview/services/services.ts @@ -1,6 +1,7 @@ "use server"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { ServicesOverviewResponse } from "./types"; @@ -14,11 +15,7 @@ export const getServicesOverview = async ({ const url = new URL(`${apiBaseUrl}/overviews/services`); - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]" && value !== undefined) { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/overview/severity-trends/severity-trends.ts b/ui/actions/overview/severity-trends/severity-trends.ts index 70701a41a8..e90efa15b7 100644 --- a/ui/actions/overview/severity-trends/severity-trends.ts +++ b/ui/actions/overview/severity-trends/severity-trends.ts @@ -5,6 +5,7 @@ import { type TimeRange, } from "@/app/(prowler)/_overview/severity-over-time/_constants/time-range.constants"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { adaptSeverityTrendsResponse } from "./severity-trends.adapter"; @@ -27,11 +28,7 @@ const getFindingsSeverityTrends = async ({ const url = new URL(`${apiBaseUrl}/overviews/findings_severity/timeseries`); - Object.entries(filters).forEach(([key, value]) => { - if (value !== undefined) { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/overview/threat-score/threat-score.ts b/ui/actions/overview/threat-score/threat-score.ts index 3a78d1ecda..0a1d7505d1 100644 --- a/ui/actions/overview/threat-score/threat-score.ts +++ b/ui/actions/overview/threat-score/threat-score.ts @@ -1,6 +1,7 @@ "use server"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; export const getThreatScore = async ({ @@ -12,12 +13,7 @@ export const getThreatScore = async ({ const url = new URL(`${apiBaseUrl}/overviews/threatscore`); - // Handle multiple filters - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]") { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/providers/providers.ts b/ui/actions/providers/providers.ts index 4665e9db39..63c01332b1 100644 --- a/ui/actions/providers/providers.ts +++ b/ui/actions/providers/providers.ts @@ -6,6 +6,7 @@ import { redirect } from "next/navigation"; import { apiBaseUrl, getAuthHeaders, getFormValue, wait } from "@/lib"; import { buildSecretConfig } from "@/lib/provider-credentials/build-crendentials"; import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; +import { appendSanitizedProviderInFilters } from "@/lib/provider-filters"; import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper"; import { ProvidersApiResponse, ProviderType } from "@/types/providers"; @@ -15,6 +16,12 @@ export const getProviders = async ({ sort = "", filters = {}, pageSize = 10, +}: { + page?: number; + query?: string; + sort?: string; + filters?: Record; + pageSize?: number; }): Promise => { const headers = await getAuthHeaders({ contentType: false }); @@ -27,12 +34,7 @@ export const getProviders = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - // Handle multiple filters - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]") { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderInFilters(url, filters); try { const response = await fetch(url.toString(), { @@ -60,7 +62,7 @@ export const getAllProviders = async ({ }: { query?: string; sort?: string; - filters?: Record; + filters?: Record; } = {}): Promise => { const headers = await getAuthHeaders({ contentType: false }); const pageSize = 100; // Use larger page size to minimize API calls @@ -79,11 +81,7 @@ export const getAllProviders = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]") { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderInFilters(url, filters); const response = await fetch(url.toString(), { headers }); const data = (await handleApiResponse(response)) as diff --git a/ui/actions/resources/resources.ts b/ui/actions/resources/resources.ts index f7b0a1b002..d65505704f 100644 --- a/ui/actions/resources/resources.ts +++ b/ui/actions/resources/resources.ts @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; export const getResources = async ({ @@ -17,7 +18,7 @@ export const getResources = async ({ page?: number; query?: string; sort?: string; - filters?: Record; + filters?: Record; pageSize?: number; include?: string; fields?: string[]; @@ -38,9 +39,7 @@ export const getResources = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - url.searchParams.append(key, String(value)); - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { @@ -66,7 +65,7 @@ export const getLatestResources = async ({ page?: number; query?: string; sort?: string; - filters?: Record; + filters?: Record; pageSize?: number; include?: string; fields?: string[]; @@ -87,9 +86,7 @@ export const getLatestResources = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - url.searchParams.append(key, String(value)); - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { @@ -107,6 +104,10 @@ export const getMetadataInfo = async ({ query = "", sort = "", filters = {}, +}: { + query?: string; + sort?: string; + filters?: Record; }) => { const headers = await getAuthHeaders({ contentType: false }); @@ -115,9 +116,7 @@ export const getMetadataInfo = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - url.searchParams.append(key, String(value)); - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { @@ -135,6 +134,10 @@ export const getLatestMetadataInfo = async ({ query = "", sort = "", filters = {}, +}: { + query?: string; + sort?: string; + filters?: Record; }) => { const headers = await getAuthHeaders({ contentType: false }); @@ -143,9 +146,7 @@ export const getLatestMetadataInfo = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - url.searchParams.append(key, String(value)); - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/scans/scans.ts b/ui/actions/scans/scans.ts index a5f615bfe4..52da71bb3b 100644 --- a/ui/actions/scans/scans.ts +++ b/ui/actions/scans/scans.ts @@ -7,6 +7,10 @@ import { COMPLIANCE_REPORT_DISPLAY_NAMES, type ComplianceReportType, } from "@/lib/compliance/compliance-report-types"; +import { + appendSanitizedProviderTypeFilters, + sanitizeProviderTypesCsv, +} from "@/lib/provider-filters"; import { addScanOperation } from "@/lib/sentry-breadcrumbs"; import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper"; export const getScans = async ({ @@ -35,13 +39,7 @@ export const getScans = async ({ url.searchParams.append(`fields[${key}]`, String(value)); }); - // Add dynamic filters (e.g., "filter[state]", "fields[scans]") - Object.entries(filters).forEach(([key, value]) => { - // Skip filter[search] since it's already added via the `query` param above - if (key !== "filter[search]") { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { headers }); @@ -58,6 +56,10 @@ export const getScansByState = async () => { const url = new URL(`${apiBaseUrl}/scans`); // Request only the necessary fields to optimize the response url.searchParams.append("fields[scans]", "state"); + url.searchParams.append( + "filter[provider_type__in]", + sanitizeProviderTypesCsv(), + ); try { const response = await fetch(url.toString(), { diff --git a/ui/app/(prowler)/providers/page.tsx b/ui/app/(prowler)/providers/page.tsx index 5cc1582364..6e52449c04 100644 --- a/ui/app/(prowler)/providers/page.tsx +++ b/ui/app/(prowler)/providers/page.tsx @@ -13,7 +13,7 @@ import { } from "@/components/providers/table"; import { ContentLayout } from "@/components/ui"; import { DataTable } from "@/components/ui/table"; -import { PROVIDER_TYPES, ProviderProps, SearchParamsProps } from "@/types"; +import { ProviderProps, SearchParamsProps } from "@/types"; export default async function Providers({ searchParams, @@ -89,22 +89,15 @@ const ProvidersTable = async ({ return acc; }, {}) || {}; - // Exclude provider types not yet supported in the UI const enrichedProviders = - providersData?.data - ?.filter((provider: ProviderProps) => - (PROVIDER_TYPES as readonly string[]).includes( - provider.attributes.provider, - ), - ) - .map((provider: ProviderProps) => { - const groupNames = - provider.relationships?.provider_groups?.data?.map( - (group: { id: string }) => - providerGroupDict[group.id] || "Unknown Group", - ) || []; - return { ...provider, groupNames }; - }) || []; + providersData?.data?.map((provider: ProviderProps) => { + const groupNames = + provider.relationships?.provider_groups?.data?.map( + (group: { id: string }) => + providerGroupDict[group.id] || "Unknown Group", + ) || []; + return { ...provider, groupNames }; + }) || []; return ( <> diff --git a/ui/lib/provider-filters.ts b/ui/lib/provider-filters.ts new file mode 100644 index 0000000000..62edc5bb64 --- /dev/null +++ b/ui/lib/provider-filters.ts @@ -0,0 +1,144 @@ +import { PROVIDER_TYPES, type ProviderType } from "@/types/providers"; + +export type ProviderFilterValue = string | string[] | undefined; +export type ProviderFilters = Record; + +interface AppendProviderFiltersOptions { + ensuredInFilterKey?: string; + excludedKeys?: string[]; + excludedKeyIncludes?: string[]; +} + +type AppendSanitizedProviderTypeFiltersOptions = Omit< + AppendProviderFiltersOptions, + "ensuredInFilterKey" +>; + +const SUPPORTED_PROVIDER_TYPES_CSV = PROVIDER_TYPES.join(","); +export const PROVIDER_IN_FILTER_KEY = "filter[provider__in]"; +export const PROVIDER_TYPE_IN_FILTER_KEY = "filter[provider_type__in]"; + +const PROVIDER_TYPE_IN_KEYS = new Set([ + PROVIDER_TYPE_IN_FILTER_KEY, + "provider_type__in", +]); + +const PROVIDER_SINGLE_KEYS = new Set([ + "filter[provider_type]", + "provider_type", +]); + +const toCsvString = (value: unknown): string => { + if (Array.isArray(value)) return value.join(","); + if (typeof value === "string") return value; + return ""; +}; + +const isSupportedProviderType = (value: string): value is ProviderType => + (PROVIDER_TYPES as readonly string[]).includes(value); + +export const sanitizeProviderTypesCsv = (value?: unknown): string => { + const rawValue = toCsvString(value); + if (!rawValue.trim()) return SUPPORTED_PROVIDER_TYPES_CSV; + + const supportedProviderTypes = Array.from( + new Set( + rawValue + .split(",") + .map((providerType) => providerType.trim()) + .filter(isSupportedProviderType), + ), + ); + + return supportedProviderTypes.length > 0 + ? supportedProviderTypes.join(",") + : SUPPORTED_PROVIDER_TYPES_CSV; +}; + +export const sanitizeProviderType = ( + value?: unknown, +): ProviderType | undefined => { + const rawValue = toCsvString(value); + if (!rawValue.trim()) return undefined; + + return rawValue + .split(",") + .map((providerType) => providerType.trim()) + .find(isSupportedProviderType); +}; + +export const sanitizeProviderFilters = ( + filters: ProviderFilters = {}, + ensuredInFilterKey?: string, +): ProviderFilters => { + const sanitizedFilters: ProviderFilters = { ...filters }; + + Object.keys(sanitizedFilters).forEach((key) => { + if (PROVIDER_TYPE_IN_KEYS.has(key)) { + sanitizedFilters[key] = sanitizeProviderTypesCsv(sanitizedFilters[key]); + return; + } + + if (PROVIDER_SINGLE_KEYS.has(key)) { + const providerType = sanitizeProviderType(sanitizedFilters[key]); + if (providerType) { + sanitizedFilters[key] = providerType; + } else { + delete sanitizedFilters[key]; + } + } + }); + + if (ensuredInFilterKey) { + sanitizedFilters[ensuredInFilterKey] = sanitizeProviderTypesCsv( + sanitizedFilters[ensuredInFilterKey], + ); + } + + return sanitizedFilters; +}; + +export const appendSanitizedProviderFilters = ( + url: URL, + filters: ProviderFilters = {}, + { + ensuredInFilterKey, + excludedKeys = ["filter[search]"], + excludedKeyIncludes = [], + }: AppendProviderFiltersOptions = {}, +): void => { + const sanitizedFilters = sanitizeProviderFilters(filters, ensuredInFilterKey); + const excludedKeysSet = new Set(excludedKeys); + + Object.entries(sanitizedFilters).forEach(([key, value]) => { + if ( + value === undefined || + excludedKeysSet.has(key) || + excludedKeyIncludes.some((excludedKey) => key.includes(excludedKey)) + ) { + return; + } + + url.searchParams.append(key, String(value)); + }); +}; + +export const appendSanitizedProviderTypeFilters = ( + url: URL, + filters: ProviderFilters = {}, + options: AppendSanitizedProviderTypeFiltersOptions = {}, +): void => + appendSanitizedProviderFilters(url, filters, { + ...options, + ensuredInFilterKey: PROVIDER_TYPE_IN_FILTER_KEY, + }); + +export const appendSanitizedProviderInFilters = ( + url: URL, + filters: ProviderFilters = {}, + options: AppendSanitizedProviderTypeFiltersOptions = {}, +): void => + appendSanitizedProviderFilters(url, filters, { + ...options, + ensuredInFilterKey: PROVIDER_IN_FILTER_KEY, + });