Address greptile comments

This commit is contained in:
Carlos Monastyrski
2025-09-15 16:15:11 -03:00
parent 5fc88610e4
commit a1aab68461
5 changed files with 58 additions and 88 deletions
@@ -9,16 +9,6 @@ const versionSchema = z
.min(1) .min(1)
.max(50) .max(50)
.regex(new RE2(/^[a-zA-Z0-9._/-]+$/), "Invalid version format"); .regex(new RE2(/^[a-zA-Z0-9._/-]+$/), "Invalid version format");
const booleanSchema = z.boolean().default(false);
const queryBooleanSchema = z
.union([z.boolean(), z.string()])
.transform((val) => {
if (typeof val === "string") {
return val === "true" || val === "1";
}
return val;
})
.default(false);
export const registerUpgradePathRouter = async (server: FastifyZodProvider) => { export const registerUpgradePathRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -28,9 +18,6 @@ export const registerUpgradePathRouter = async (server: FastifyZodProvider) => {
rateLimit: publicEndpointLimit rateLimit: publicEndpointLimit
}, },
schema: { schema: {
querystring: z.object({
includePrerelease: queryBooleanSchema
}),
response: { response: {
200: z.object({ 200: z.object({
versions: z.array( versions: z.array(
@@ -47,8 +34,7 @@ export const registerUpgradePathRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req) => { handler: async (req) => {
try { try {
const { includePrerelease } = req.query; const versions = await req.server.services.upgradePath.getGitHubReleases();
const versions = await req.server.services.upgradePath.getGitHubReleases(includePrerelease);
return { return {
versions versions
@@ -72,8 +58,7 @@ export const registerUpgradePathRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
body: z.object({ body: z.object({
fromVersion: versionSchema, fromVersion: versionSchema,
toVersion: versionSchema, toVersion: versionSchema
includePrerelease: booleanSchema
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -112,15 +97,9 @@ export const registerUpgradePathRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req) => { handler: async (req) => {
try { try {
const { fromVersion, toVersion, includePrerelease } = req.body; const { fromVersion, toVersion } = req.body;
req.log.info({ fromVersion, toVersion, includePrerelease }, "Calculating upgrade path"); const result = await req.server.services.upgradePath.calculateUpgradePath(fromVersion, toVersion);
const result = await req.server.services.upgradePath.calculateUpgradePath(
fromVersion,
toVersion,
includePrerelease
);
req.log.info( req.log.info(
{ pathLength: result.path.length, hasBreaking: result.breakingChanges.length > 0 }, { pathLength: result.path.length, hasBreaking: result.breakingChanges.length > 0 },
@@ -19,17 +19,15 @@ const versionSchema = z
.min(1) .min(1)
.max(50) .max(50)
.regex(new RE2(/^[a-zA-Z0-9._/-]+$/), "Invalid version format"); .regex(new RE2(/^[a-zA-Z0-9._/-]+$/), "Invalid version format");
const booleanSchema = z.boolean().default(false);
interface CalculateUpgradePathParams { interface CalculateUpgradePathParams {
fromVersion: string; fromVersion: string;
toVersion: string; toVersion: string;
includePrerelease?: boolean;
} }
export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFactory) => { export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFactory) => {
const getGitHubReleases = async (includePrerelease = false): Promise<FormattedRelease[]> => { const getGitHubReleases = async (): Promise<FormattedRelease[]> => {
const cacheKey = `upgrade-path:releases:${includePrerelease}`; const cacheKey = "upgrade-path:releases";
try { try {
const cached = await keyStore.getItem(cacheKey); const cached = await keyStore.getItem(cacheKey);
@@ -39,7 +37,7 @@ export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFacto
} }
try { try {
const releases = await fetchReleases(booleanSchema.parse(includePrerelease)); const releases = await fetchReleases(false);
const filteredReleases = releases.filter((v) => !v.tagName.includes("nightly")); const filteredReleases = releases.filter((v) => !v.tagName.includes("nightly"));
@@ -62,13 +60,19 @@ export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFacto
try { try {
const yamlPath = path.join(__dirname, "..", "..", "..", "upgrade-path.yaml"); const yamlPath = path.join(__dirname, "..", "..", "..", "upgrade-path.yaml");
const resolvedPath = path.resolve(yamlPath);
const expectedBaseDir = path.resolve(__dirname, "..", "..", "..");
if (!resolvedPath.startsWith(expectedBaseDir)) {
throw new Error("Invalid configuration file path");
}
const yamlContent = await readFile(yamlPath, "utf8"); const yamlContent = await readFile(yamlPath, "utf8");
if (yamlContent.length > 1024 * 1024) { if (yamlContent.length > 1024 * 1024) {
throw new Error("Config file too large"); throw new Error("Config file too large");
} }
const config = yaml.load(yamlContent) as UpgradePathConfig; const config = yaml.load(yamlContent, { schema: yaml.FAILSAFE_SCHEMA }) as UpgradePathConfig;
const versionConfig = config?.versions || {}; const versionConfig = config?.versions || {};
await keyStore.setItemWithExpiry(cacheKey, 24 * 60 * 60, JSON.stringify(versionConfig)); await keyStore.setItemWithExpiry(cacheKey, 24 * 60 * 60, JSON.stringify(versionConfig));
@@ -85,7 +89,8 @@ export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFacto
const normalizeVersion = (version: string): string => { const normalizeVersion = (version: string): string => {
// Extract just the X.X.X.X part from any version format // Extract just the X.X.X.X part from any version format
const versionMatch = version.match(/(\d+\.\d+\.\d+(?:\.\d+)?)/); const versionRegex = new RE2(/(\d+\.\d+\.\d+(?:\.\d+)?)/);
const versionMatch = version.match(versionRegex);
if (versionMatch) { if (versionMatch) {
return versionMatch[1]; return versionMatch[1];
} }
@@ -122,7 +127,7 @@ export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFacto
}; };
const validateParams = (params: CalculateUpgradePathParams) => { const validateParams = (params: CalculateUpgradePathParams) => {
const { fromVersion, toVersion, includePrerelease = false } = params; const { fromVersion, toVersion } = params;
versionSchema.parse(fromVersion); versionSchema.parse(fromVersion);
versionSchema.parse(toVersion); versionSchema.parse(toVersion);
@@ -135,12 +140,12 @@ export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFacto
throw new Error("Nightly releases are not supported for upgrade path calculation"); throw new Error("Nightly releases are not supported for upgrade path calculation");
} }
return { fromVersion, toVersion, includePrerelease: booleanSchema.parse(includePrerelease) }; return { fromVersion, toVersion };
}; };
const calculateUpgradePath = async (params: CalculateUpgradePathParams): Promise<UpgradePathResult> => { const calculateUpgradePath = async (params: CalculateUpgradePathParams): Promise<UpgradePathResult> => {
const { fromVersion, toVersion, includePrerelease } = validateParams(params); const { fromVersion, toVersion } = validateParams(params);
const cacheKey = `upgrade-path:${fromVersion}:${toVersion}:${includePrerelease}`; const cacheKey = `upgrade-path:${fromVersion}:${toVersion}`;
try { try {
const cached = await keyStore.getItem(cacheKey); const cached = await keyStore.getItem(cacheKey);
@@ -149,7 +154,7 @@ export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFacto
// Cache miss, continue to fetch from source // Cache miss, continue to fetch from source
} }
const [releases, config] = await Promise.all([getGitHubReleases(includePrerelease), getUpgradePathConfig()]); const [releases, config] = await Promise.all([getGitHubReleases(), getUpgradePathConfig()]);
const cleanFrom = normalizeVersion(fromVersion); const cleanFrom = normalizeVersion(fromVersion);
const cleanTo = normalizeVersion(toVersion); const cleanTo = normalizeVersion(toVersion);
@@ -264,7 +269,6 @@ export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFacto
return { return {
getGitHubReleases, getGitHubReleases,
getUpgradePathConfig, getUpgradePathConfig,
calculateUpgradePath: (fromVersion: string, toVersion: string, includePrerelease = false) => calculateUpgradePath: (fromVersion: string, toVersion: string) => calculateUpgradePath({ fromVersion, toVersion })
calculateUpgradePath({ fromVersion, toVersion, includePrerelease })
}; };
}; };
+1 -6
View File
@@ -1,7 +1,2 @@
export type { export type { CalculateUpgradePathParams, GitHubVersion, UpgradePathResult } from "./queries";
CalculateUpgradePathParams,
GetUpgradePathVersionsParams,
GitHubVersion,
UpgradePathResult
} from "./queries";
export { useCalculateUpgradePath, useGetUpgradePathVersions } from "./queries"; export { useCalculateUpgradePath, useGetUpgradePathVersions } from "./queries";
+13 -21
View File
@@ -1,4 +1,4 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { useMutation, useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
@@ -35,36 +35,26 @@ export interface UpgradePathResult {
config: Record<string, unknown>; config: Record<string, unknown>;
} }
export interface GetUpgradePathVersionsParams {
includePrerelease?: boolean;
}
export interface CalculateUpgradePathParams { export interface CalculateUpgradePathParams {
fromVersion: string; fromVersion: string;
toVersion: string; toVersion: string;
includePrerelease?: boolean;
} }
const upgradePathKeys = { const upgradePathKeys = {
all: ["upgrade-path"] as const, all: ["upgrade-path"] as const,
versions: (params: GetUpgradePathVersionsParams) => versions: () => [...upgradePathKeys.all, "versions"] as const,
[...upgradePathKeys.all, "versions", params] as const,
calculate: (params: CalculateUpgradePathParams) => calculate: (params: CalculateUpgradePathParams) =>
[...upgradePathKeys.all, "calculate", params] as const [...upgradePathKeys.all, "calculate", params] as const
}; };
export const useGetUpgradePathVersions = ( export const useGetUpgradePathVersions = (
params: GetUpgradePathVersionsParams,
options?: Omit<UseQueryOptions<{ versions: GitHubVersion[] }>, "queryKey" | "queryFn"> options?: Omit<UseQueryOptions<{ versions: GitHubVersion[] }>, "queryKey" | "queryFn">
) => { ) => {
return useQuery({ return useQuery({
queryKey: upgradePathKeys.versions(params), queryKey: upgradePathKeys.versions(),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<{ versions: GitHubVersion[] }>( const { data } = await apiRequest.get<{ versions: GitHubVersion[] }>(
"/api/v1/upgrade-path/versions", "/api/v1/upgrade-path/versions"
{
params
}
); );
return data; return data;
}, },
@@ -73,11 +63,13 @@ export const useGetUpgradePathVersions = (
}; };
export const useCalculateUpgradePath = () => { export const useCalculateUpgradePath = () => {
return async (params: CalculateUpgradePathParams): Promise<UpgradePathResult> => { return useMutation({
const { data } = await apiRequest.post<UpgradePathResult>( mutationFn: async (params: CalculateUpgradePathParams): Promise<UpgradePathResult> => {
"/api/v1/upgrade-path/calculate", const { data } = await apiRequest.post<UpgradePathResult>(
params "/api/v1/upgrade-path/calculate",
); params
return data; );
}; return data;
}
});
}; };
@@ -1,10 +1,9 @@
/* eslint-disable no-nested-ternary */ /* eslint-disable no-nested-ternary */
import { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { SingleValue } from "react-select"; import { SingleValue } from "react-select";
import { faExternalLink } from "@fortawesome/free-solid-svg-icons"; import { faExternalLink } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useMutation } from "@tanstack/react-query";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FilterableSelect, FormControl } from "@app/components/v2"; import { Button, FilterableSelect, FormControl } from "@app/components/v2";
@@ -60,29 +59,31 @@ export const UpgradePathPage = () => {
data: versions, data: versions,
isLoading: versionsLoading, isLoading: versionsLoading,
isFetching: versionsFetching isFetching: versionsFetching
} = useGetUpgradePathVersions( } = useGetUpgradePathVersions({
{ enabled: true,
includePrerelease: false staleTime: 24 * 60 * 60 * 1000,
}, refetchOnWindowFocus: false
{ });
enabled: true,
staleTime: 24 * 60 * 60 * 1000,
refetchOnWindowFocus: false
}
);
const calculateMutation = useMutation({ const calculateMutation = useCalculateUpgradePath();
mutationFn: useCalculateUpgradePath(),
onSuccess: (data) => { // Handle mutation results
setUpgradeResult(data); React.useEffect(() => {
}, if (calculateMutation.isSuccess && calculateMutation.data) {
onError: (error: unknown) => { setUpgradeResult(calculateMutation.data);
}
}, [calculateMutation.isSuccess, calculateMutation.data]);
React.useEffect(() => {
if (calculateMutation.isError) {
createNotification({ createNotification({
text: (error as any)?.response?.data?.message || "Failed to calculate upgrade path", text:
(calculateMutation.error as any)?.response?.data?.message ||
"Failed to calculate upgrade path",
type: "error" type: "error"
}); });
} }
}); }, [calculateMutation.isError, calculateMutation.error]);
const versionOptions = useMemo(() => { const versionOptions = useMemo(() => {
if (!versions?.versions) return []; if (!versions?.versions) return [];
@@ -125,8 +126,7 @@ export const UpgradePathPage = () => {
calculateMutation.mutate({ calculateMutation.mutate({
fromVersion, fromVersion,
toVersion, toVersion
includePrerelease: false
}); });
}; };