migrate to monorepo?
This commit is contained in:
21
packages/api-server/package.json
Normal file
21
packages/api-server/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@marketplace-scrapers/api-server",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"module": "./src/index.ts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "bun ./src/index.ts",
|
||||
"dev": "bun --watch ./src/index.ts",
|
||||
"build": "bun build ./src/index.ts --target=bun --outdir=../../dist/api"
|
||||
},
|
||||
"dependencies": {
|
||||
"@marketplace-scrapers/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
30
packages/api-server/src/index.ts
Normal file
30
packages/api-server/src/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { statusRoute } from "./routes/status";
|
||||
import { kijijiRoute } from "./routes/kijiji";
|
||||
import { facebookRoute } from "./routes/facebook";
|
||||
import { ebayRoute } from "./routes/ebay";
|
||||
|
||||
const PORT = process.env.PORT || 4005;
|
||||
|
||||
const server = Bun.serve({
|
||||
port: PORT as number | string,
|
||||
idleTimeout: 0,
|
||||
routes: {
|
||||
// Health check endpoint
|
||||
"/api/status": statusRoute,
|
||||
|
||||
// Marketplace search endpoints
|
||||
"/api/kijiji": kijijiRoute,
|
||||
"/api/facebook": facebookRoute,
|
||||
"/api/ebay": ebayRoute,
|
||||
|
||||
// Fallback for unmatched /api routes
|
||||
"/api/*": Response.json({ message: "Not found" }, { status: 404 }),
|
||||
},
|
||||
|
||||
// Fallback for all other routes
|
||||
fetch(req: Request) {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`API Server running on ${server.hostname}:${server.port}`);
|
||||
56
packages/api-server/src/routes/ebay.ts
Normal file
56
packages/api-server/src/routes/ebay.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { fetchEbayItems } from "@marketplace-scrapers/core";
|
||||
|
||||
/**
|
||||
* GET /api/ebay?q={query}&minPrice={minPrice}&maxPrice={maxPrice}&strictMode={strictMode}&exclusions={exclusions}&keywords={keywords}
|
||||
* Search eBay for listings
|
||||
*/
|
||||
export async function ebayRoute(req: Request): Promise<Response> {
|
||||
const reqUrl = new URL(req.url);
|
||||
|
||||
const SEARCH_QUERY =
|
||||
req.headers.get("query") || reqUrl.searchParams.get("q") || null;
|
||||
if (!SEARCH_QUERY)
|
||||
return Response.json(
|
||||
{
|
||||
message:
|
||||
"Request didn't have 'query' header or 'q' search parameter!",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
|
||||
// Parse optional parameters with defaults
|
||||
const minPrice = reqUrl.searchParams.get("minPrice")
|
||||
? parseInt(reqUrl.searchParams.get("minPrice")!)
|
||||
: undefined;
|
||||
const maxPrice = reqUrl.searchParams.get("maxPrice")
|
||||
? parseInt(reqUrl.searchParams.get("maxPrice")!)
|
||||
: undefined;
|
||||
const strictMode = reqUrl.searchParams.get("strictMode") === "true";
|
||||
const exclusionsParam = reqUrl.searchParams.get("exclusions");
|
||||
const exclusions = exclusionsParam ? exclusionsParam.split(",").map(s => s.trim()) : [];
|
||||
const keywordsParam = reqUrl.searchParams.get("keywords");
|
||||
const keywords = keywordsParam ? keywordsParam.split(",").map(s => s.trim()) : [SEARCH_QUERY];
|
||||
|
||||
try {
|
||||
const items = await fetchEbayItems(SEARCH_QUERY, 5, {
|
||||
minPrice,
|
||||
maxPrice,
|
||||
strictMode,
|
||||
exclusions,
|
||||
keywords,
|
||||
});
|
||||
if (!items || items.length === 0)
|
||||
return Response.json(
|
||||
{ message: "Search didn't return any results!" },
|
||||
{ status: 404 },
|
||||
);
|
||||
return Response.json(items, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("eBay scraping error:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||
return Response.json(
|
||||
{ message: errorMessage },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
40
packages/api-server/src/routes/facebook.ts
Normal file
40
packages/api-server/src/routes/facebook.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { fetchFacebookItems } from "@marketplace-scrapers/core";
|
||||
|
||||
/**
|
||||
* GET /api/facebook?q={query}&location={location}&cookies={cookies}
|
||||
* Search Facebook Marketplace for listings
|
||||
*/
|
||||
export async function facebookRoute(req: Request): Promise<Response> {
|
||||
const reqUrl = new URL(req.url);
|
||||
|
||||
const SEARCH_QUERY =
|
||||
req.headers.get("query") || reqUrl.searchParams.get("q") || null;
|
||||
if (!SEARCH_QUERY)
|
||||
return Response.json(
|
||||
{
|
||||
message:
|
||||
"Request didn't have 'query' header or 'q' search parameter!",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
|
||||
const LOCATION = reqUrl.searchParams.get("location") || "toronto";
|
||||
const COOKIES_SOURCE = reqUrl.searchParams.get("cookies") || undefined;
|
||||
|
||||
try {
|
||||
const items = await fetchFacebookItems(SEARCH_QUERY, 5, LOCATION, 25, COOKIES_SOURCE);
|
||||
if (!items || items.length === 0)
|
||||
return Response.json(
|
||||
{ message: "Search didn't return any results!" },
|
||||
{ status: 404 },
|
||||
);
|
||||
return Response.json(items, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Facebook scraping error:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||
return Response.json(
|
||||
{ message: errorMessage },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
37
packages/api-server/src/routes/kijiji.ts
Normal file
37
packages/api-server/src/routes/kijiji.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { fetchKijijiItems } from "@marketplace-scrapers/core";
|
||||
|
||||
/**
|
||||
* GET /api/kijiji?q={query}
|
||||
* Search Kijiji marketplace for listings
|
||||
*/
|
||||
export async function kijijiRoute(req: Request): Promise<Response> {
|
||||
const reqUrl = new URL(req.url);
|
||||
|
||||
const SEARCH_QUERY =
|
||||
req.headers.get("query") || reqUrl.searchParams.get("q") || null;
|
||||
if (!SEARCH_QUERY)
|
||||
return Response.json(
|
||||
{
|
||||
message:
|
||||
"Request didn't have 'query' header or 'q' search parameter!",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
|
||||
try {
|
||||
const items = await fetchKijijiItems(SEARCH_QUERY, 5);
|
||||
if (!items)
|
||||
return Response.json(
|
||||
{ message: "Search didn't return any results!" },
|
||||
{ status: 404 },
|
||||
);
|
||||
return Response.json(items, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Kijiji scraping error:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||
return Response.json(
|
||||
{ message: errorMessage },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
6
packages/api-server/src/routes/status.ts
Normal file
6
packages/api-server/src/routes/status.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Health check endpoint
|
||||
*/
|
||||
export function statusRoute(): Response {
|
||||
return new Response("OK", { status: 200 });
|
||||
}
|
||||
13
packages/api-server/tsconfig.json
Normal file
13
packages/api-server/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"strict": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user