284 lines
7.4 KiB
TypeScript
284 lines
7.4 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server"
|
|
|
|
/**
|
|
* eBay API Proxy Route
|
|
* Proxies requests to eBay Finding API to avoid CORS issues
|
|
*/
|
|
|
|
interface eBaySearchParams {
|
|
keywords: string
|
|
categoryId?: string
|
|
sortOrder?: string
|
|
maxResults?: number
|
|
}
|
|
|
|
interface eBaySearchResult {
|
|
itemId: string
|
|
title: string
|
|
price: string
|
|
currency: string
|
|
imageUrl?: string
|
|
viewItemUrl: string
|
|
condition?: string
|
|
shippingCost?: string
|
|
affiliateLink: string
|
|
}
|
|
|
|
type MaybeArray<T> = T | T[]
|
|
|
|
const SEARCH_CACHE_TTL = 15 * 60 * 1000 // 15 minutes
|
|
const searchResponseCache = new Map<
|
|
string,
|
|
{ results: eBaySearchResult[]; timestamp: number }
|
|
>()
|
|
const inFlightSearchResponses = new Map<string, Promise<eBaySearchResult[]>>()
|
|
|
|
// Affiliate campaign ID for generating links
|
|
const AFFILIATE_CAMPAIGN_ID =
|
|
process.env.EBAY_AFFILIATE_CAMPAIGN_ID?.trim() || ""
|
|
|
|
// Generate eBay affiliate link
|
|
function generateAffiliateLink(viewItemUrl: string): string {
|
|
if (!AFFILIATE_CAMPAIGN_ID) {
|
|
return viewItemUrl
|
|
}
|
|
|
|
try {
|
|
const url = new URL(viewItemUrl)
|
|
url.searchParams.set("mkcid", "1")
|
|
url.searchParams.set("mkrid", "711-53200-19255-0")
|
|
url.searchParams.set("siteid", "0")
|
|
url.searchParams.set("campid", AFFILIATE_CAMPAIGN_ID)
|
|
url.searchParams.set("toolid", "10001")
|
|
url.searchParams.set("mkevt", "1")
|
|
return url.toString()
|
|
} catch {
|
|
return viewItemUrl
|
|
}
|
|
}
|
|
|
|
function first<T>(value: MaybeArray<T> | undefined): T | undefined {
|
|
if (!value) {
|
|
return undefined
|
|
}
|
|
|
|
return Array.isArray(value) ? value[0] : value
|
|
}
|
|
|
|
function normalizeItem(item: any): eBaySearchResult {
|
|
const currentPrice = first(item.sellingStatus?.currentPrice)
|
|
const shippingCost = first(item.shippingInfo?.shippingServiceCost)
|
|
const condition = first(item.condition)
|
|
const viewItemUrl = item.viewItemURL || item.viewItemUrl || ""
|
|
|
|
return {
|
|
itemId: item.itemId || "",
|
|
title: item.title || "Unknown Item",
|
|
price: `${currentPrice?.value || "0"} ${currentPrice?.currencyId || "USD"}`,
|
|
currency: currentPrice?.currencyId || "USD",
|
|
imageUrl: first(item.galleryURL) || undefined,
|
|
viewItemUrl,
|
|
condition: condition?.conditionDisplayName || undefined,
|
|
shippingCost: shippingCost?.value
|
|
? `${shippingCost.value} ${shippingCost.currencyId || currentPrice?.currencyId || "USD"}`
|
|
: undefined,
|
|
affiliateLink: generateAffiliateLink(viewItemUrl),
|
|
}
|
|
}
|
|
|
|
async function readEbayErrorMessage(response: Response) {
|
|
const text = await response.text().catch(() => "")
|
|
if (!text.trim()) {
|
|
return `eBay API error: ${response.status}`
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(text) as any
|
|
const messages = parsed?.errorMessage?.[0]?.error?.[0]
|
|
const message = Array.isArray(messages?.message)
|
|
? messages.message[0]
|
|
: messages?.message
|
|
|
|
if (typeof message === "string" && message.trim()) {
|
|
const errorId = Array.isArray(messages?.errorId)
|
|
? messages.errorId[0]
|
|
: messages?.errorId
|
|
return errorId
|
|
? `eBay API error ${errorId}: ${message}`
|
|
: `eBay API error: ${message}`
|
|
}
|
|
} catch {
|
|
// Fall through to returning the raw text below.
|
|
}
|
|
|
|
return text.trim() || `eBay API error: ${response.status}`
|
|
}
|
|
|
|
function buildCacheKey(
|
|
keywords: string,
|
|
categoryId: string | undefined,
|
|
sortOrder: string,
|
|
maxResults: number
|
|
): string {
|
|
return [
|
|
keywords.trim().toLowerCase(),
|
|
categoryId || "",
|
|
sortOrder || "BestMatch",
|
|
String(maxResults),
|
|
].join("|")
|
|
}
|
|
|
|
function getCachedSearchResults(cacheKey: string): eBaySearchResult[] | null {
|
|
const cached = searchResponseCache.get(cacheKey)
|
|
|
|
if (!cached) {
|
|
return null
|
|
}
|
|
|
|
if (Date.now() - cached.timestamp > SEARCH_CACHE_TTL) {
|
|
searchResponseCache.delete(cacheKey)
|
|
return null
|
|
}
|
|
|
|
return cached.results
|
|
}
|
|
|
|
function setCachedSearchResults(cacheKey: string, results: eBaySearchResult[]) {
|
|
searchResponseCache.set(cacheKey, {
|
|
results,
|
|
timestamp: Date.now(),
|
|
})
|
|
}
|
|
|
|
export async function GET(request: NextRequest) {
|
|
const { searchParams } = new URL(request.url)
|
|
|
|
const keywords = searchParams.get("keywords")
|
|
const categoryId = searchParams.get("categoryId") || undefined
|
|
const sortOrder = searchParams.get("sortOrder") || "BestMatch"
|
|
const maxResults = parseInt(searchParams.get("maxResults") || "6", 10)
|
|
const cacheKey = buildCacheKey(
|
|
keywords || "",
|
|
categoryId,
|
|
sortOrder,
|
|
maxResults
|
|
)
|
|
|
|
if (!keywords) {
|
|
return NextResponse.json(
|
|
{ error: "Keywords parameter is required" },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
const appId = process.env.EBAY_APP_ID?.trim()
|
|
|
|
if (!appId) {
|
|
console.error("EBAY_APP_ID not configured")
|
|
return NextResponse.json(
|
|
{
|
|
error:
|
|
"eBay API not configured. Please set EBAY_APP_ID environment variable.",
|
|
},
|
|
{ status: 503 }
|
|
)
|
|
}
|
|
|
|
const cachedResults = getCachedSearchResults(cacheKey)
|
|
if (cachedResults) {
|
|
return NextResponse.json(cachedResults)
|
|
}
|
|
|
|
const inFlight = inFlightSearchResponses.get(cacheKey)
|
|
if (inFlight) {
|
|
try {
|
|
const results = await inFlight
|
|
return NextResponse.json(results)
|
|
} catch (error) {
|
|
return NextResponse.json(
|
|
{
|
|
error:
|
|
error instanceof Error
|
|
? error.message
|
|
: "Failed to fetch products from eBay",
|
|
},
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|
|
|
|
// Build eBay Finding API URL
|
|
const baseUrl = "https://svcs.ebay.com/services/search/FindingService/v1"
|
|
const url = new URL(baseUrl)
|
|
|
|
url.searchParams.set("OPERATION-NAME", "findItemsAdvanced")
|
|
url.searchParams.set("SERVICE-VERSION", "1.0.0")
|
|
url.searchParams.set("SECURITY-APPNAME", appId)
|
|
url.searchParams.set("RESPONSE-DATA-FORMAT", "JSON")
|
|
url.searchParams.set("REST-PAYLOAD", "true")
|
|
url.searchParams.set("keywords", keywords)
|
|
url.searchParams.set("sortOrder", sortOrder)
|
|
url.searchParams.set("paginationInput.entriesPerPage", maxResults.toString())
|
|
|
|
if (categoryId) {
|
|
url.searchParams.set("categoryId", categoryId)
|
|
}
|
|
|
|
try {
|
|
const request = (async () => {
|
|
const response = await fetch(url.toString(), {
|
|
method: "GET",
|
|
headers: {
|
|
Accept: "application/json",
|
|
},
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorMessage = await readEbayErrorMessage(response)
|
|
throw new Error(errorMessage)
|
|
}
|
|
|
|
const data = await response.json()
|
|
|
|
// Parse eBay API response
|
|
const findItemsAdvancedResponse = data.findItemsAdvancedResponse?.[0]
|
|
if (!findItemsAdvancedResponse) {
|
|
return []
|
|
}
|
|
|
|
const searchResult = findItemsAdvancedResponse.searchResult?.[0]
|
|
if (
|
|
!searchResult ||
|
|
!searchResult.item ||
|
|
searchResult.item.length === 0
|
|
) {
|
|
return []
|
|
}
|
|
|
|
const items = Array.isArray(searchResult.item)
|
|
? searchResult.item
|
|
: [searchResult.item]
|
|
|
|
return items.map((item: any) => normalizeItem(item))
|
|
})()
|
|
|
|
inFlightSearchResponses.set(cacheKey, request)
|
|
|
|
const results = await request
|
|
setCachedSearchResults(cacheKey, results)
|
|
return NextResponse.json(results)
|
|
} catch (error) {
|
|
console.error("Error fetching from eBay API:", error)
|
|
return NextResponse.json(
|
|
{
|
|
error:
|
|
error instanceof Error
|
|
? error.message
|
|
: "Failed to fetch products from eBay",
|
|
},
|
|
{ status: 500 }
|
|
)
|
|
} finally {
|
|
inFlightSearchResponses.delete(cacheKey)
|
|
}
|
|
}
|