api / src /index.ts
RohanVashisht's picture
Update src/index.ts
6fd0abf verified
raw
history blame
7.64 kB
import { serve } from "bun";
import { readFile } from "fs/promises";
import path from "path";
// Type Definition for Items (Packages/Programs)
interface Item {
name?: string;
full_name?: string;
owner?: string;
description?: string;
topics?: string[];
created_at?: string;
usage_count?: number;
}
// --- CONFIGURATION ---
const DB_PATH = "../database/database";
const PORT = 7860;
const RELOAD_INTERVAL_MS = 3600000; // 1 hour
const corsHeaders = {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
};
// --- GLOBAL DATA STORE (in-memory) ---
let packages: Item[] = [];
let programs: Item[] = [];
let sorted: {
packages: { latest: Item[]; mostUsed: Item[] };
programs: { latest: Item[]; mostUsed: Item[] };
} = { packages: { latest: [], mostUsed: [] }, programs: { latest: [], mostUsed: [] } };
// --- UTILITY FUNCTIONS ---
function getSortedData(packages: Item[], programs: Item[]) {
return {
packages: {
latest: [...packages].sort(sortByDate),
mostUsed: [...packages].sort(sortByUsage),
},
programs: {
latest: [...programs].sort(sortByDate),
mostUsed: [...programs].sort(sortByUsage),
},
};
}
function sortByDate(a: Item, b: Item): number {
return new Date(b.created_at || "").getTime() - new Date(a.created_at || "").getTime();
}
function sortByUsage(a: Item, b: Item): number {
return (b.usage_count || 0) - (a.usage_count || 0);
}
/**
* Filters an array of items by query and topic filter.
*/
function filterItems(items: Item[], query: string | null, topic: string | null): Item[] {
return items.filter(({ name, full_name, description, topics }) => {
if (topic && !topics?.some(t => t.toLowerCase() === topic)) return false;
if (!query) return true;
const lowerQ = query.toLowerCase();
return [name, full_name, description, ...(topics || [])]
.some(field => field?.toLowerCase().includes(lowerQ));
});
}
/**
* Returns a paginated slice of items.
*/
function getPaginated(items: Item[], page: number = 0, size: number = 10): Item[] {
const start = page * size;
return items.slice(start, start + size);
}
/**
* Finds an item by owner and repo name.
*/
function findItem(items: Item[], owner: string, repo: string): Item | undefined {
return items.find(({ owner: o, name, full_name }) =>
(o?.toLowerCase() === owner && name?.toLowerCase() === repo) ||
full_name?.toLowerCase() === `${owner}/${repo}`
);
}
/**
* Parses a range string of the form "start..end" to [start, end].
*/
function parseRange(str: string | null, max: number): [number, number] {
const match = str?.match(/^(\d+)\.\.(\d+)$/);
const [start, end] = match
? [parseInt(match[1]), parseInt(match[2])]
: [0, 10];
return [Math.max(0, start), Math.min(max, end)];
}
/**
* Loads data from disk and updates in-memory database and sorted views.
*/
async function reloadData() {
try {
const pkgPath = path.resolve(__dirname, `${DB_PATH}/packages.json`);
const prgPath = path.resolve(__dirname, `${DB_PATH}/programs.json`);
const [pkgData, prgData] = await Promise.all([
readFile(pkgPath, "utf8"),
readFile(prgPath, "utf8"),
]);
packages = JSON.parse(pkgData);
programs = JSON.parse(prgData);
sorted = getSortedData(packages, programs);
console.log(`[${new Date().toISOString()}] Database reloaded`);
} catch (err) {
console.error("Error reloading database files:", err);
}
}
// Initial data load and set up periodic reload
reloadData();
setInterval(reloadData, RELOAD_INTERVAL_MS);
// --- SERVER DEFINITION ---
serve({
port: PORT,
fetch(req: Request): Response | Promise<Response> {
const url = new URL(req.url);
const { pathname, searchParams } = url;
const q = searchParams.get("q")?.trim().toLowerCase() ?? null;
const filter = searchParams.get("filter")?.trim().toLowerCase() ?? null;
// Handle preflight CORS requests
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
...corsHeaders,
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}
// --- API ENDPOINTS ---
// Search endpoints
if (pathname === "/api/searchPackages") {
const result = filterItems(packages, q, filter).slice(0, 25);
return Response.json(result, { headers: corsHeaders });
}
if (pathname === "/api/searchPrograms") {
const result = filterItems(programs, q, filter).slice(0, 25);
return Response.json(result, { headers: corsHeaders });
}
// Infinite scroll endpoints
if (pathname === "/api/infiniteScrollPackages") {
const page = parseInt(searchParams.get("pageNumber") || "0", 10);
if (isNaN(page) || page < 0)
return Response.json({ error: "Invalid page number" }, { status: 400, headers: corsHeaders });
return Response.json(getPaginated(packages, page), { headers: corsHeaders });
}
if (pathname === "/api/infiniteScrollPrograms") {
const page = parseInt(searchParams.get("pageNumber") || "0", 10);
if (isNaN(page) || page < 0)
return Response.json({ error: "Invalid page number" }, { status: 400, headers: corsHeaders });
return Response.json(getPaginated(programs, page), { headers: corsHeaders });
}
// Item details endpoints (by owner/repo)
const programMatch = pathname.match(/^\/api\/programs\/([^\/]+)\/([^\/]+)$/);
if (programMatch) {
const [_, owner, repo] = programMatch.map(s => s.toLowerCase());
const found = findItem(programs, owner, repo);
return Response.json(found || { error: "Program not found" }, {
status: found ? 200 : 404,
headers: corsHeaders,
});
}
const packageMatch = pathname.match(/^\/api\/packages\/([^\/]+)\/([^\/]+)$/);
if (packageMatch) {
const [_, owner, repo] = packageMatch.map(s => s.toLowerCase());
const found = findItem(packages, owner, repo);
return Response.json(found || { error: "Package not found" }, {
status: found ? 200 : 404,
headers: corsHeaders,
});
}
// Index details endpoints (for homepage sections)
if (pathname === "/api/indexDetailsPackages") {
const section = searchParams.get("section");
if (section !== "latestRepos" && section !== "mostUsed") {
return Response.json({ error: "Invalid section" }, { status: 400, headers: corsHeaders });
}
const sortKey = section === "latestRepos" ? "latest" : "mostUsed";
const data = sorted.packages[sortKey as keyof typeof sorted.packages] ?? packages;
const [start, end] = parseRange(searchParams.get("range"), data.length);
return Response.json(data.slice(start, end), { headers: corsHeaders });
}
if (pathname === "/api/indexDetailsPrograms") {
const section = searchParams.get("section");
if (section !== "latestRepos" && section !== "mostUsed") {
return Response.json({ error: "Invalid section" }, { status: 400, headers: corsHeaders });
}
const sortKey = section === "latestRepos" ? "latest" : "mostUsed";
const data = sorted.programs[sortKey as keyof typeof sorted.programs] ?? programs;
const [start, end] = parseRange(searchParams.get("range"), data.length);
return Response.json(data.slice(start, end), { headers: corsHeaders });
}
// Unknown endpoint
return new Response("Not Found", {
status: 404,
headers: corsHeaders,
});
},
});
console.log(`Server running on http://localhost:${PORT}`);