import { useContext } from "react"; import { useQuery } from "react-query"; import RegistryPathContext from "../contexts/RegistryPathContext"; import { useDataMode } from "../contexts/DataModeContext"; import restFetch from "./restApiClient"; import { RestApiError } from "./restApiClient"; import { FEAST_FV_TYPES, genericFVType } from "../parsers/mergedFVTypes"; interface ResourceQueryOptions { resourceType: string; project?: string; restPath: string; restSelect?: (data: any) => T | undefined; enabled?: boolean; } /** * Generic hook for fetching a specific resource slice via REST API. * * Each caller fires its own lightweight endpoint request, and react-query * deduplicates identical keys automatically. */ function useResourceQuery({ resourceType, project, restPath, restSelect, enabled = true, }: ResourceQueryOptions) { const registryUrl = useContext(RegistryPathContext); const { fetchOptions } = useDataMode(); const query = useQuery( ["rest", resourceType, registryUrl, project || "all"], () => restFetch(registryUrl, restPath, fetchOptions), { enabled: !!registryUrl && enabled, staleTime: 30_000, select: restSelect, retry: (failureCount, error) => { if (error instanceof RestApiError && error.status === 403) return false; return failureCount < 3; }, }, ); const isPermissionDenied = query.isError && query.error instanceof RestApiError && query.error.status === 403; return { ...query, isPermissionDenied }; } // --------------------------------------------------------------------------- // REST endpoint path builders // --------------------------------------------------------------------------- function entityListPath(project?: string): string { if (project && project !== "all") { return `/entities?project=${encodeURIComponent(project)}&include_relationships=true`; } return "/entities/all?limit=100&include_relationships=true"; } function entityDetailPath(name: string, project: string): string { return `/entities/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}&include_relationships=true`; } function featureViewListPath(project?: string): string { if (project && project !== "all") { return `/feature_views?project=${encodeURIComponent(project)}&include_relationships=true`; } return "/feature_views/all?limit=100&include_relationships=true"; } function featureViewDetailPath(name: string, project: string): string { return `/feature_views/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}&include_relationships=true`; } function featureServiceListPath(project?: string): string { if (project && project !== "all") { return `/feature_services?project=${encodeURIComponent(project)}&include_relationships=true`; } return "/feature_services/all?limit=100&include_relationships=true"; } function featureServiceDetailPath(name: string, project: string): string { return `/feature_services/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}&include_relationships=true`; } function dataSourceListPath(project?: string): string { if (project && project !== "all") { return `/data_sources?project=${encodeURIComponent(project)}&include_relationships=true`; } return "/data_sources/all?limit=100&include_relationships=true"; } function dataSourceDetailPath(name: string, project: string): string { return `/data_sources/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}&include_relationships=true`; } function savedDatasetListPath(project?: string): string { if (project && project !== "all") { return `/saved_datasets?project=${encodeURIComponent(project)}`; } return "/saved_datasets/all?limit=100"; } function savedDatasetDetailPath(name: string, project: string): string { return `/saved_datasets/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}`; } function labelViewListPath(project?: string): string { if (project && project !== "all") { return `/label_views?project=${encodeURIComponent(project)}&include_relationships=true`; } return "/label_views/all?limit=100&include_relationships=true"; } function labelViewDetailPath(name: string, project: string): string { return `/label_views/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}&include_relationships=true`; } function permissionListPath(project?: string): string { if (project && project !== "all") { return `/permissions?project=${encodeURIComponent(project)}`; } return `/permissions?project=default`; } function featuresListPath(project?: string): string { if (project && project !== "all") { return `/features?project=${encodeURIComponent(project)}`; } return "/features/all?limit=100"; } function featureDetailPath( featureViewName: string, featureName: string, project: string, ): string { return `/features/${encodeURIComponent(featureViewName)}/${encodeURIComponent(featureName)}?project=${encodeURIComponent(project)}`; } // --------------------------------------------------------------------------- // REST response → mergedFVList converter // --------------------------------------------------------------------------- function restFeatureViewsToMergedList(resp: any): genericFVType[] { const featureViews = resp?.featureViews || []; return featureViews .filter((fv: any) => fv.type !== "labelView") .map((fv: any) => { const fvType = fv.type; if (fvType === "onDemandFeatureView") { return { name: fv.spec?.name, type: FEAST_FV_TYPES.ondemand, features: fv.spec?.features || [], object: fv, }; } if (fvType === "streamFeatureView") { return { name: fv.spec?.name, type: FEAST_FV_TYPES.stream, features: fv.spec?.features || [], object: fv, }; } return { name: fv.spec?.name, type: FEAST_FV_TYPES.regular, features: fv.spec?.features || [], object: fv, }; }); } function restLabelViewsFromResponse(resp: any): any[] { const featureViews = resp?.featureViews || []; return featureViews.filter((fv: any) => fv.type === "labelView"); } function restFeatureViewDetailToGeneric(resp: any): genericFVType | undefined { if (!resp || !resp.spec) return undefined; const fvType = resp.type; if (fvType === "onDemandFeatureView") { return { name: resp.spec.name, type: FEAST_FV_TYPES.ondemand, features: resp.spec.features || [], object: resp, }; } if (fvType === "streamFeatureView") { return { name: resp.spec.name, type: FEAST_FV_TYPES.stream, features: resp.spec.features || [], object: resp, }; } if (fvType === "labelView") { return { name: resp.spec.name, type: FEAST_FV_TYPES.label, features: resp.spec.features || [], object: resp, }; } return { name: resp.spec.name, type: FEAST_FV_TYPES.regular, features: resp.spec.features || [], object: resp, }; } export default useResourceQuery; export { entityListPath, entityDetailPath, featureViewListPath, featureViewDetailPath, featureServiceListPath, featureServiceDetailPath, dataSourceListPath, dataSourceDetailPath, savedDatasetListPath, savedDatasetDetailPath, labelViewListPath, labelViewDetailPath, permissionListPath, featuresListPath, featureDetailPath, restFeatureViewsToMergedList, restFeatureViewDetailToGeneric, restLabelViewsFromResponse, };