From 8463e9ea1c4c9f9767ee364a615f753791187e9d Mon Sep 17 00:00:00 2001 From: zetaloop Date: Sat, 28 Feb 2026 12:17:52 +0800 Subject: [PATCH] feat(search): migrate to backend endpoint --- app/api/search/route.ts | 62 -------------------------------------- lib/api/search.ts | 29 +++++++++++------- tests/search-route.test.ts | 51 ------------------------------- 3 files changed, 18 insertions(+), 124 deletions(-) delete mode 100644 app/api/search/route.ts delete mode 100644 tests/search-route.test.ts diff --git a/app/api/search/route.ts b/app/api/search/route.ts deleted file mode 100644 index bf83825..0000000 --- a/app/api/search/route.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { mockPlayers, mockServices, mockShops } from "@/lib/mock" -import { searchCatalog } from "@/lib/search/search-catalog" -import type { SearchSort } from "@/lib/search/types" -import { NextResponse } from "next/server" - -export const dynamic = "force-dynamic" - -const SEARCH_SORTS: ReadonlySet = new Set([ - "composite", - "rating", - "orders", - "price_asc", - "price_desc", -]) - -function numberParam(value: string | null, fallback: number) { - if (value === null) return fallback - const parsed = Number(value) - return Number.isFinite(parsed) ? parsed : fallback -} - -export async function GET(request: Request) { - const { searchParams } = new URL(request.url) - - const q = searchParams.get("q") ?? undefined - const selectedGames = searchParams.getAll("game") - const min = searchParams.get("min") ?? "" - const max = searchParams.get("max") ?? "" - const onlyOnline = (searchParams.get("online") ?? "0") === "1" - const minRating = searchParams.get("minRating") ?? "0" - - const sortParam = (searchParams.get("sort") ?? "composite") as SearchSort - const sort: SearchSort = SEARCH_SORTS.has(sortParam) ? sortParam : "composite" - - const limit = numberParam(searchParams.get("limit"), 12) - const offset = numberParam(searchParams.get("offset"), 0) - - const response = searchCatalog( - { - q, - selectedGames, - min, - max, - onlyOnline, - minRating, - sort, - limit, - offset, - }, - { - players: mockPlayers, - shops: mockShops, - services: mockServices, - }, - ) - - return NextResponse.json(response, { - headers: { - "Cache-Control": "no-store", - }, - }) -} diff --git a/lib/api/search.ts b/lib/api/search.ts index 4c9b4d9..0c8198e 100644 --- a/lib/api/search.ts +++ b/lib/api/search.ts @@ -1,5 +1,8 @@ +import { isApiError } from "@/lib/errors" import type { SearchResponse, SearchSort } from "@/lib/search/types" +import { httpJson } from "./http" + export interface SearchCatalogParams { q?: string selectedGames?: string[] @@ -19,26 +22,30 @@ export async function searchCatalog(params: SearchCatalogParams): Promise(`/api/v1/search?${searchParams.toString()}`, { + cache: "no-store", + signal: params.signal, + }) + } catch (error) { + if (error instanceof Error && error.message === "UNAUTHORIZED") { + throw error + } + if (isApiError(error)) { + throw new Error(`Search API request failed: ${error.code} ${error.msg}`) + } + throw error } - - return (await res.json()) as SearchResponse } diff --git a/tests/search-route.test.ts b/tests/search-route.test.ts deleted file mode 100644 index 21d93ac..0000000 --- a/tests/search-route.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { GET } from "@/app/api/search/route" -import { describe, expect, it } from "vitest" - -describe("GET /api/search", () => { - it("returns 200 with items and meta", async () => { - const request = new Request("http://x/api/search") - const response = await GET(request) - - expect(response.status).toBe(200) - const json = await response.json() - expect(Array.isArray(json.items)).toBe(true) - expect(json.meta).toHaveProperty("total") - expect(json.meta).toHaveProperty("offset") - expect(json.meta).toHaveProperty("limit") - }) - - it("sets Cache-Control to no-store", async () => { - const request = new Request("http://x/api/search") - const response = await GET(request) - - expect(response.headers.get("Cache-Control")).toBe("no-store") - }) - - it("filters by q param", async () => { - const request = new Request("http://x/api/search?q=winter") - const response = await GET(request) - const json = await response.json() - - expect(response.status).toBe(200) - expect(json.items.length).toBeGreaterThan(0) - }) - - it("respects limit and offset", async () => { - const request = new Request("http://x/api/search?limit=2&offset=1") - const response = await GET(request) - const json = await response.json() - - expect(json.items.length).toBeLessThanOrEqual(2) - expect(json.meta.limit).toBe(2) - expect(json.meta.offset).toBe(1) - }) - - it("handles game filter", async () => { - const request = new Request("http://x/api/search?game=CS2") - const response = await GET(request) - const json = await response.json() - - expect(response.status).toBe(200) - expect(json.items.length).toBeGreaterThan(0) - }) -})