Update src/index.ts
Browse files- src/index.ts +91 -34
src/index.ts
CHANGED
@@ -1,7 +1,8 @@
|
|
1 |
import { serve } from "bun";
|
2 |
-
import
|
3 |
-
import
|
4 |
|
|
|
5 |
interface Item {
|
6 |
name?: string;
|
7 |
full_name?: string;
|
@@ -12,43 +13,70 @@ interface Item {
|
|
12 |
usage_count?: number;
|
13 |
}
|
14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
const corsHeaders = {
|
16 |
"Content-Type": "application/json",
|
17 |
"Access-Control-Allow-Origin": "*",
|
18 |
};
|
19 |
|
20 |
-
|
21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
|
23 |
-
|
24 |
-
(b.
|
|
|
25 |
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
mostUsed: [...packages].sort(sortByUsage),
|
30 |
-
},
|
31 |
-
programs: {
|
32 |
-
latest: [...programs].sort(sortByDate),
|
33 |
-
mostUsed: [...programs].sort(sortByUsage),
|
34 |
-
},
|
35 |
-
};
|
36 |
|
37 |
-
|
|
|
|
|
|
|
38 |
return items.filter(({ name, full_name, description, topics }) => {
|
39 |
-
if (
|
40 |
-
if (!
|
41 |
-
const lowerQ =
|
42 |
return [name, full_name, description, ...(topics || [])]
|
43 |
.some(field => field?.toLowerCase().includes(lowerQ));
|
44 |
});
|
45 |
}
|
46 |
|
|
|
|
|
|
|
47 |
function getPaginated(items: Item[], page: number = 0, size: number = 10): Item[] {
|
48 |
const start = page * size;
|
49 |
return items.slice(start, start + size);
|
50 |
}
|
51 |
|
|
|
|
|
|
|
52 |
function findItem(items: Item[], owner: string, repo: string): Item | undefined {
|
53 |
return items.find(({ owner: o, name, full_name }) =>
|
54 |
(o?.toLowerCase() === owner && name?.toLowerCase() === repo) ||
|
@@ -56,6 +84,9 @@ function findItem(items: Item[], owner: string, repo: string): Item | undefined
|
|
56 |
);
|
57 |
}
|
58 |
|
|
|
|
|
|
|
59 |
function parseRange(str: string | null, max: number): [number, number] {
|
60 |
const match = str?.match(/^(\d+)\.\.(\d+)$/);
|
61 |
const [start, end] = match
|
@@ -64,14 +95,41 @@ function parseRange(str: string | null, max: number): [number, number] {
|
|
64 |
return [Math.max(0, start), Math.min(max, end)];
|
65 |
}
|
66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
serve({
|
68 |
-
port:
|
69 |
fetch(req: Request): Response | Promise<Response> {
|
70 |
const url = new URL(req.url);
|
71 |
const { pathname, searchParams } = url;
|
72 |
const q = searchParams.get("q")?.trim().toLowerCase() ?? null;
|
73 |
const filter = searchParams.get("filter")?.trim().toLowerCase() ?? null;
|
74 |
|
|
|
75 |
if (req.method === "OPTIONS") {
|
76 |
return new Response(null, {
|
77 |
status: 204,
|
@@ -83,32 +141,33 @@ serve({
|
|
83 |
});
|
84 |
}
|
85 |
|
|
|
|
|
|
|
86 |
if (pathname === "/api/searchPackages") {
|
87 |
const result = filterItems(packages, q, filter).slice(0, 25);
|
88 |
return Response.json(result, { headers: corsHeaders });
|
89 |
}
|
90 |
-
|
91 |
if (pathname === "/api/searchPrograms") {
|
92 |
const result = filterItems(programs, q, filter).slice(0, 25);
|
93 |
return Response.json(result, { headers: corsHeaders });
|
94 |
}
|
95 |
|
|
|
96 |
if (pathname === "/api/infiniteScrollPackages") {
|
97 |
const page = parseInt(searchParams.get("pageNumber") || "0", 10);
|
98 |
if (isNaN(page) || page < 0)
|
99 |
return Response.json({ error: "Invalid page number" }, { status: 400, headers: corsHeaders });
|
100 |
-
|
101 |
return Response.json(getPaginated(packages, page), { headers: corsHeaders });
|
102 |
}
|
103 |
-
|
104 |
if (pathname === "/api/infiniteScrollPrograms") {
|
105 |
const page = parseInt(searchParams.get("pageNumber") || "0", 10);
|
106 |
if (isNaN(page) || page < 0)
|
107 |
return Response.json({ error: "Invalid page number" }, { status: 400, headers: corsHeaders });
|
108 |
-
|
109 |
return Response.json(getPaginated(programs, page), { headers: corsHeaders });
|
110 |
}
|
111 |
|
|
|
112 |
const programMatch = pathname.match(/^\/api\/programs\/([^\/]+)\/([^\/]+)$/);
|
113 |
if (programMatch) {
|
114 |
const [_, owner, repo] = programMatch.map(s => s.toLowerCase());
|
@@ -118,7 +177,6 @@ serve({
|
|
118 |
headers: corsHeaders,
|
119 |
});
|
120 |
}
|
121 |
-
|
122 |
const packageMatch = pathname.match(/^\/api\/packages\/([^\/]+)\/([^\/]+)$/);
|
123 |
if (packageMatch) {
|
124 |
const [_, owner, repo] = packageMatch.map(s => s.toLowerCase());
|
@@ -129,30 +187,29 @@ serve({
|
|
129 |
});
|
130 |
}
|
131 |
|
|
|
132 |
if (pathname === "/api/indexDetailsPackages") {
|
133 |
-
|
134 |
if (section !== "latestRepos" && section !== "mostUsed") {
|
135 |
return Response.json({ error: "Invalid section" }, { status: 400, headers: corsHeaders });
|
136 |
}
|
137 |
-
// Map 'latestRepos' to 'latest' for sorting
|
138 |
const sortKey = section === "latestRepos" ? "latest" : "mostUsed";
|
139 |
-
|
140 |
const [start, end] = parseRange(searchParams.get("range"), data.length);
|
141 |
return Response.json(data.slice(start, end), { headers: corsHeaders });
|
142 |
}
|
143 |
-
|
144 |
if (pathname === "/api/indexDetailsPrograms") {
|
145 |
-
|
146 |
if (section !== "latestRepos" && section !== "mostUsed") {
|
147 |
return Response.json({ error: "Invalid section" }, { status: 400, headers: corsHeaders });
|
148 |
}
|
149 |
-
// Map 'latestRepos' to 'latest' for sorting
|
150 |
const sortKey = section === "latestRepos" ? "latest" : "mostUsed";
|
151 |
-
|
152 |
const [start, end] = parseRange(searchParams.get("range"), data.length);
|
153 |
return Response.json(data.slice(start, end), { headers: corsHeaders });
|
154 |
}
|
155 |
|
|
|
156 |
return new Response("Not Found", {
|
157 |
status: 404,
|
158 |
headers: corsHeaders,
|
@@ -160,4 +217,4 @@ serve({
|
|
160 |
},
|
161 |
});
|
162 |
|
163 |
-
console.log(
|
|
|
1 |
import { serve } from "bun";
|
2 |
+
import { readFile } from "fs/promises";
|
3 |
+
import path from "path";
|
4 |
|
5 |
+
// Type Definition for Items (Packages/Programs)
|
6 |
interface Item {
|
7 |
name?: string;
|
8 |
full_name?: string;
|
|
|
13 |
usage_count?: number;
|
14 |
}
|
15 |
|
16 |
+
// --- CONFIGURATION ---
|
17 |
+
|
18 |
+
const DB_PATH = "../database/database";
|
19 |
+
const PORT = 7860;
|
20 |
+
const RELOAD_INTERVAL_MS = 3600000; // 1 hour
|
21 |
+
|
22 |
const corsHeaders = {
|
23 |
"Content-Type": "application/json",
|
24 |
"Access-Control-Allow-Origin": "*",
|
25 |
};
|
26 |
|
27 |
+
// --- GLOBAL DATA STORE (in-memory) ---
|
28 |
+
|
29 |
+
let packages: Item[] = require(`${DB_PATH}/packages.json`);
|
30 |
+
let programs: Item[] = require(`${DB_PATH}/programs.json`);
|
31 |
+
let sorted = getSortedData(packages, programs);
|
32 |
+
|
33 |
+
// --- UTILITY FUNCTIONS ---
|
34 |
+
|
35 |
+
function getSortedData(packages: Item[], programs: Item[]) {
|
36 |
+
return {
|
37 |
+
packages: {
|
38 |
+
latest: [...packages].sort(sortByDate),
|
39 |
+
mostUsed: [...packages].sort(sortByUsage),
|
40 |
+
},
|
41 |
+
programs: {
|
42 |
+
latest: [...programs].sort(sortByDate),
|
43 |
+
mostUsed: [...programs].sort(sortByUsage),
|
44 |
+
},
|
45 |
+
};
|
46 |
+
}
|
47 |
|
48 |
+
function sortByDate(a: Item, b: Item): number {
|
49 |
+
return new Date(b.created_at || "").getTime() - new Date(a.created_at || "").getTime();
|
50 |
+
}
|
51 |
|
52 |
+
function sortByUsage(a: Item, b: Item): number {
|
53 |
+
return (b.usage_count || 0) - (a.usage_count || 0);
|
54 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
|
56 |
+
/**
|
57 |
+
* Filters an array of items by query and topic filter.
|
58 |
+
*/
|
59 |
+
function filterItems(items: Item[], query: string | null, topic: string | null): Item[] {
|
60 |
return items.filter(({ name, full_name, description, topics }) => {
|
61 |
+
if (topic && !topics?.some(t => t.toLowerCase() === topic)) return false;
|
62 |
+
if (!query) return true;
|
63 |
+
const lowerQ = query.toLowerCase();
|
64 |
return [name, full_name, description, ...(topics || [])]
|
65 |
.some(field => field?.toLowerCase().includes(lowerQ));
|
66 |
});
|
67 |
}
|
68 |
|
69 |
+
/**
|
70 |
+
* Returns a paginated slice of items.
|
71 |
+
*/
|
72 |
function getPaginated(items: Item[], page: number = 0, size: number = 10): Item[] {
|
73 |
const start = page * size;
|
74 |
return items.slice(start, start + size);
|
75 |
}
|
76 |
|
77 |
+
/**
|
78 |
+
* Finds an item by owner and repo name.
|
79 |
+
*/
|
80 |
function findItem(items: Item[], owner: string, repo: string): Item | undefined {
|
81 |
return items.find(({ owner: o, name, full_name }) =>
|
82 |
(o?.toLowerCase() === owner && name?.toLowerCase() === repo) ||
|
|
|
84 |
);
|
85 |
}
|
86 |
|
87 |
+
/**
|
88 |
+
* Parses a range string of the form "start..end" to [start, end].
|
89 |
+
*/
|
90 |
function parseRange(str: string | null, max: number): [number, number] {
|
91 |
const match = str?.match(/^(\d+)\.\.(\d+)$/);
|
92 |
const [start, end] = match
|
|
|
95 |
return [Math.max(0, start), Math.min(max, end)];
|
96 |
}
|
97 |
|
98 |
+
/**
|
99 |
+
* Loads data from disk and updates in-memory database and sorted views.
|
100 |
+
*/
|
101 |
+
async function reloadData() {
|
102 |
+
try {
|
103 |
+
const pkgPath = path.resolve(__dirname, `${DB_PATH}/packages.json`);
|
104 |
+
const prgPath = path.resolve(__dirname, `${DB_PATH}/programs.json`);
|
105 |
+
const [pkgData, prgData] = await Promise.all([
|
106 |
+
readFile(pkgPath, "utf8"),
|
107 |
+
readFile(prgPath, "utf8"),
|
108 |
+
]);
|
109 |
+
packages = JSON.parse(pkgData);
|
110 |
+
programs = JSON.parse(prgData);
|
111 |
+
sorted = getSortedData(packages, programs);
|
112 |
+
console.log(`[${new Date().toISOString()}] Database reloaded`);
|
113 |
+
} catch (err) {
|
114 |
+
console.error("Error reloading database files:", err);
|
115 |
+
}
|
116 |
+
}
|
117 |
+
|
118 |
+
// Initial data load and set up periodic reload
|
119 |
+
reloadData();
|
120 |
+
setInterval(reloadData, RELOAD_INTERVAL_MS);
|
121 |
+
|
122 |
+
// --- SERVER DEFINITION ---
|
123 |
+
|
124 |
serve({
|
125 |
+
port: PORT,
|
126 |
fetch(req: Request): Response | Promise<Response> {
|
127 |
const url = new URL(req.url);
|
128 |
const { pathname, searchParams } = url;
|
129 |
const q = searchParams.get("q")?.trim().toLowerCase() ?? null;
|
130 |
const filter = searchParams.get("filter")?.trim().toLowerCase() ?? null;
|
131 |
|
132 |
+
// Handle preflight CORS requests
|
133 |
if (req.method === "OPTIONS") {
|
134 |
return new Response(null, {
|
135 |
status: 204,
|
|
|
141 |
});
|
142 |
}
|
143 |
|
144 |
+
// --- API ENDPOINTS ---
|
145 |
+
|
146 |
+
// Search endpoints
|
147 |
if (pathname === "/api/searchPackages") {
|
148 |
const result = filterItems(packages, q, filter).slice(0, 25);
|
149 |
return Response.json(result, { headers: corsHeaders });
|
150 |
}
|
|
|
151 |
if (pathname === "/api/searchPrograms") {
|
152 |
const result = filterItems(programs, q, filter).slice(0, 25);
|
153 |
return Response.json(result, { headers: corsHeaders });
|
154 |
}
|
155 |
|
156 |
+
// Infinite scroll endpoints
|
157 |
if (pathname === "/api/infiniteScrollPackages") {
|
158 |
const page = parseInt(searchParams.get("pageNumber") || "0", 10);
|
159 |
if (isNaN(page) || page < 0)
|
160 |
return Response.json({ error: "Invalid page number" }, { status: 400, headers: corsHeaders });
|
|
|
161 |
return Response.json(getPaginated(packages, page), { headers: corsHeaders });
|
162 |
}
|
|
|
163 |
if (pathname === "/api/infiniteScrollPrograms") {
|
164 |
const page = parseInt(searchParams.get("pageNumber") || "0", 10);
|
165 |
if (isNaN(page) || page < 0)
|
166 |
return Response.json({ error: "Invalid page number" }, { status: 400, headers: corsHeaders });
|
|
|
167 |
return Response.json(getPaginated(programs, page), { headers: corsHeaders });
|
168 |
}
|
169 |
|
170 |
+
// Item details endpoints (by owner/repo)
|
171 |
const programMatch = pathname.match(/^\/api\/programs\/([^\/]+)\/([^\/]+)$/);
|
172 |
if (programMatch) {
|
173 |
const [_, owner, repo] = programMatch.map(s => s.toLowerCase());
|
|
|
177 |
headers: corsHeaders,
|
178 |
});
|
179 |
}
|
|
|
180 |
const packageMatch = pathname.match(/^\/api\/packages\/([^\/]+)\/([^\/]+)$/);
|
181 |
if (packageMatch) {
|
182 |
const [_, owner, repo] = packageMatch.map(s => s.toLowerCase());
|
|
|
187 |
});
|
188 |
}
|
189 |
|
190 |
+
// Index details endpoints (for homepage sections)
|
191 |
if (pathname === "/api/indexDetailsPackages") {
|
192 |
+
const section = searchParams.get("section");
|
193 |
if (section !== "latestRepos" && section !== "mostUsed") {
|
194 |
return Response.json({ error: "Invalid section" }, { status: 400, headers: corsHeaders });
|
195 |
}
|
|
|
196 |
const sortKey = section === "latestRepos" ? "latest" : "mostUsed";
|
197 |
+
const data = sorted.packages[sortKey as keyof typeof sorted.packages] ?? packages;
|
198 |
const [start, end] = parseRange(searchParams.get("range"), data.length);
|
199 |
return Response.json(data.slice(start, end), { headers: corsHeaders });
|
200 |
}
|
|
|
201 |
if (pathname === "/api/indexDetailsPrograms") {
|
202 |
+
const section = searchParams.get("section");
|
203 |
if (section !== "latestRepos" && section !== "mostUsed") {
|
204 |
return Response.json({ error: "Invalid section" }, { status: 400, headers: corsHeaders });
|
205 |
}
|
|
|
206 |
const sortKey = section === "latestRepos" ? "latest" : "mostUsed";
|
207 |
+
const data = sorted.programs[sortKey as keyof typeof sorted.programs] ?? programs;
|
208 |
const [start, end] = parseRange(searchParams.get("range"), data.length);
|
209 |
return Response.json(data.slice(start, end), { headers: corsHeaders });
|
210 |
}
|
211 |
|
212 |
+
// Unknown endpoint
|
213 |
return new Response("Not Found", {
|
214 |
status: 404,
|
215 |
headers: corsHeaders,
|
|
|
217 |
},
|
218 |
});
|
219 |
|
220 |
+
console.log(`Server running on http://localhost:${PORT}`);
|