From 4e5deb22a18d6ef3f45ac63a4e9773491a357e67 Mon Sep 17 00:00:00 2001 From: bkellam Date: Tue, 10 Sep 2024 12:24:12 -0700 Subject: [PATCH] Escape regex when searching for a specific filepath + some error handling / schema adjustments --- package.json | 1 + src/lib/errorCodes.ts | 1 + src/lib/schemas.ts | 8 ++++---- src/lib/server/searchService.ts | 18 ++++++++++++++---- src/lib/serviceError.ts | 8 ++++++++ yarn.lock | 5 +++++ 6 files changed, 33 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index a9bcc52c..7e4b0476 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@uiw/react-codemirror": "^4.23.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "escape-string-regexp": "^5.0.0", "http-status-codes": "^2.3.0", "lucide-react": "^0.435.0", "next": "14.2.6", diff --git a/src/lib/errorCodes.ts b/src/lib/errorCodes.ts index 54c0f5d8..4144a232 100644 --- a/src/lib/errorCodes.ts +++ b/src/lib/errorCodes.ts @@ -1,5 +1,6 @@ export enum ErrorCode { + UNEXPECTED_ERROR = 'UNEXPECTED_ERROR', MISSING_REQUIRED_QUERY_PARAMETER = 'MISSING_REQUIRED_QUERY_PARAMETER', REPOSITORY_NOT_FOUND = 'REPOSITORY_NOT_FOUND', FILE_NOT_FOUND = 'FILE_NOT_FOUND', diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index af0b0961..319475c1 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -4,13 +4,13 @@ export type SearchRequest = z.infer; export const searchRequestSchema = z.object({ query: z.string(), numResults: z.number(), - whole: z.optional(z.boolean()), + whole: z.boolean().optional(), }); export type SearchResponse = z.infer; export type SearchResult = SearchResponse["Result"]; -export type SearchResultFile = SearchResult["Files"][number]; +export type SearchResultFile = NonNullable[number]; export type SearchResultFileMatch = SearchResultFile["ChunkMatches"][number]; export type SearchResultRange = z.infer; export type SearchResultLocation = z.infer; @@ -50,8 +50,8 @@ export const searchResponseSchema = z.object({ Checksum: z.string(), Score: z.number(), // Set if `whole` is true. - Content: z.optional(z.string()), - })), + Content: z.string().optional(), + })).nullable(), }), }); diff --git a/src/lib/server/searchService.ts b/src/lib/server/searchService.ts index 636b4894..924363aa 100644 --- a/src/lib/server/searchService.ts +++ b/src/lib/server/searchService.ts @@ -1,8 +1,9 @@ import { SHARD_MAX_MATCH_COUNT, TOTAL_MAX_MATCH_COUNT } from "../environment"; import { FileSourceRequest, FileSourceResponse, SearchRequest, SearchResponse, searchResponseSchema } from "../schemas"; -import { fileNotFound, invalidZoektResponse, ServiceError } from "../serviceError"; +import { fileNotFound, invalidZoektResponse, schemaValidationError, ServiceError, unexpectedError } from "../serviceError"; import { isServiceError } from "../utils"; import { zoektFetch } from "./zoektClient"; +import escapeStringRegexp from "escape-string-regexp"; export const search = async ({ query, numResults, whole }: SearchRequest): Promise => { const body = JSON.stringify({ @@ -29,12 +30,21 @@ export const search = async ({ query, numResults, whole }: SearchRequest): Promi } const searchBody = await searchResponse.json(); - return searchResponseSchema.parse(searchBody); + const parsedSearchResponse = searchResponseSchema.safeParse(searchBody); + if (!parsedSearchResponse.success) { + console.error(`Failed to parse zoekt response. Error: ${parsedSearchResponse.error}`); + return unexpectedError(`Something went wrong while parsing the response from zoekt`); + } + + return parsedSearchResponse.data; } export const getFileSource = async ({ fileName, repository }: FileSourceRequest): Promise => { + const escapedFileName = escapeStringRegexp(fileName); + const escapedRepository = escapeStringRegexp(repository); + const searchResponse = await search({ - query: `${fileName} repo:${repository}`, + query: `${escapedFileName} repo:^${escapedRepository}$`, numResults: 1, whole: true, }); @@ -45,7 +55,7 @@ export const getFileSource = async ({ fileName, repository }: FileSourceRequest) const files = searchResponse.Result.Files; - if (files.length === 0) { + if (!files || files.length === 0) { return fileNotFound(fileName, repository); } diff --git a/src/lib/serviceError.ts b/src/lib/serviceError.ts index d7a413da..b4883913 100644 --- a/src/lib/serviceError.ts +++ b/src/lib/serviceError.ts @@ -59,4 +59,12 @@ export const fileNotFound = async (fileName: string, repository: string): Promis errorCode: ErrorCode.FILE_NOT_FOUND, message: `File "${fileName}" not found in repository "${repository}"`, }; +} + +export const unexpectedError = (message: string): ServiceError => { + return { + statusCode: StatusCodes.INTERNAL_SERVER_ERROR, + errorCode: ErrorCode.UNEXPECTED_ERROR, + message: `Unexpected error: ${message}`, + }; } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 84269e48..ad3e17ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1607,6 +1607,11 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escape-string-regexp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + eslint-config-next@14.2.6: version "14.2.6" resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-14.2.6.tgz#43623683155540feff830b08956a57ece8148d8f"