Skip to content

Commit 121b71c

Browse files
rhelmerVinnl
andauthored
feat: add MAX_SEARCH_QUERY_LENGTH for autocomplete API calls (#6222)
* feat: add MAX_SEARCH_QUERY_LENGTH for autocomplete API calls --------- Co-authored-by: Vincent <[email protected]>
1 parent 81d2b6d commit 121b71c

File tree

3 files changed

+123
-10
lines changed

3 files changed

+123
-10
lines changed

src/app/api/v1/location-autocomplete/getRelevantLocations.test.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,48 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
import { getRelevantLocations } from "./getRelevantLocations";
5+
import { logger } from "../../../functions/server/logging";
6+
import {
7+
getRelevantLocations,
8+
MAX_SEARCH_QUERY_LENGTH,
9+
} from "./getRelevantLocations";
610
import { RelevantLocation } from "./types";
711

812
describe("getRelevantLocations", () => {
13+
let loggerWarnSpy: jest.SpiedFunction<typeof logger.warn>;
14+
beforeAll(async () => {
15+
loggerWarnSpy = jest.spyOn(logger, "warn");
16+
});
17+
afterAll(async () => {
18+
loggerWarnSpy.mockRestore();
19+
});
20+
beforeEach(async () => {
21+
loggerWarnSpy.mockImplementation((..._args: unknown[]) => logger);
22+
});
23+
afterEach(async () => {
24+
loggerWarnSpy.mockClear();
25+
});
26+
27+
it("returns an empty list if the search term is not a string", () => {
28+
// Arrange:
29+
const availableLocations: RelevantLocation[] = [
30+
{
31+
id: "42",
32+
n: "Tulsa",
33+
s: "OK",
34+
},
35+
];
36+
37+
// Act:
38+
const results = getRelevantLocations(
39+
null as unknown as string,
40+
availableLocations,
41+
);
42+
43+
// Assert:
44+
expect(results).toStrictEqual([]);
45+
});
46+
947
it("filters out locations that don't match the search term", () => {
1048
// Arrange:
1149
const availableLocations: RelevantLocation[] = [
@@ -48,6 +86,50 @@ describe("getRelevantLocations", () => {
4886

4987
// Assert:
5088
expect(results).toStrictEqual([]);
89+
90+
expect(loggerWarnSpy.mock.calls).toEqual([
91+
[
92+
"location autocomplete query over max length",
93+
{ length: 0, maxLength: 128 },
94+
],
95+
]);
96+
});
97+
98+
it("returns an empty list if fuzzy search cannot find matches", () => {
99+
// Arrange:
100+
const availableLocations: RelevantLocation[] = [
101+
{
102+
id: "42",
103+
n: "Tulsa",
104+
s: "OK",
105+
},
106+
];
107+
108+
// Act:
109+
const results = getRelevantLocations("😺", availableLocations);
110+
111+
// Assert:
112+
expect(results).toStrictEqual([]);
113+
});
114+
115+
it("ignores search terms that exceed the maximum supported length", () => {
116+
// Arrange:
117+
const availableLocations: RelevantLocation[] = [
118+
{
119+
id: "42",
120+
n: "Tulsa",
121+
s: "OK",
122+
},
123+
];
124+
125+
// Act:
126+
const results = getRelevantLocations(
127+
"a".repeat(MAX_SEARCH_QUERY_LENGTH + 1),
128+
availableLocations,
129+
);
130+
131+
// Assert:
132+
expect(results).toStrictEqual([]);
51133
});
52134

53135
it("sorts the 75% most relevant results by location", () => {

src/app/api/v1/location-autocomplete/getRelevantLocations.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,32 @@
44

55
import uFuzzy from "@leeoniya/ufuzzy";
66
import { RelevantLocation } from "./types";
7+
import { logger } from "../../../functions/server/logging";
8+
9+
// Guard against pathological inputs that would make ufuzzy's regex generation hang.
10+
export const MAX_SEARCH_QUERY_LENGTH = 128;
711

812
export function getRelevantLocations(
913
searchQuery: string,
1014
knownLocations: RelevantLocation[],
1115
): RelevantLocation[] {
16+
if (typeof searchQuery !== "string") {
17+
return [];
18+
}
19+
20+
const normalizedSearchQuery = searchQuery.trim();
21+
22+
if (
23+
normalizedSearchQuery.length === 0 ||
24+
normalizedSearchQuery.length > MAX_SEARCH_QUERY_LENGTH
25+
) {
26+
logger.warn("location autocomplete query over max length", {
27+
length: normalizedSearchQuery.length,
28+
maxLength: MAX_SEARCH_QUERY_LENGTH,
29+
});
30+
return [];
31+
}
32+
1233
/**
1334
* Fully spelled-out names of locations in the US
1435
*
@@ -55,7 +76,7 @@ export function getRelevantLocations(
5576

5677
const nameIndexes = fuzzySearch.filter(
5778
locationNamesAndAlternateNames,
58-
searchQuery,
79+
normalizedSearchQuery,
5980
);
6081
if (!nameIndexes) {
6182
return [];
@@ -64,12 +85,12 @@ export function getRelevantLocations(
6485
const info = fuzzySearch.info(
6586
nameIndexes,
6687
locationNamesAndAlternateNames,
67-
searchQuery,
88+
normalizedSearchQuery,
6889
);
6990
const order = fuzzySearch.sort(
7091
info,
7192
locationNamesAndAlternateNames,
72-
searchQuery,
93+
normalizedSearchQuery,
7394
);
7495
// Since `order` contains a ranked array of indexes to the indexes of the most
7596
// closely matching names, we can retrieve the associated location data from

src/app/api/v1/location-autocomplete/route.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@ import { NextResponse, NextRequest } from "next/server";
66

77
import { RelevantLocation } from "./types";
88
// The location autocomplete data will be created during the build step.
9-
// @ts-ignore-next-line
10-
import locationData from "../../../../../locationAutocompleteData.json";
9+
import locationDataJson from "../../../../../locationAutocompleteData.json";
1110
import { getRelevantLocations } from "./getRelevantLocations";
1211

12+
type LocationDataFile = {
13+
data: RelevantLocation[];
14+
};
15+
16+
const locationData = locationDataJson as LocationDataFile | undefined;
17+
1318
export type MatchingLocations = Array<RelevantLocation> | [];
1419

1520
export interface SearchLocationParams {
@@ -32,24 +37,29 @@ function getLocationsResults({
3237
maxResults: 5,
3338
},
3439
}: SearchLocationParams): SearchLocationResults {
35-
if (!locationData) {
40+
if (!locationData?.data) {
3641
throw new Error(
3742
"No location data available: You may need to run `npm run create-location-data`.",
3843
);
3944
}
4045

4146
const { minQueryLength, maxResults } = config;
4247

48+
const normalizedSearchQuery =
49+
typeof searchQuery === "string" ? searchQuery.trim() : "";
50+
51+
const knownLocations = locationData.data;
52+
4353
const matchingLocations =
44-
searchQuery && searchQuery.length >= minQueryLength
45-
? getRelevantLocations(searchQuery, locationData.data)
54+
normalizedSearchQuery.length >= minQueryLength
55+
? getRelevantLocations(normalizedSearchQuery, knownLocations)
4656
: [];
4757

4858
const locationsResults =
4959
maxResults > 0 ? matchingLocations.slice(0, maxResults) : matchingLocations;
5060

5161
return {
52-
searchQuery,
62+
searchQuery: normalizedSearchQuery,
5363
results: locationsResults as MatchingLocations,
5464
};
5565
}

0 commit comments

Comments
 (0)