🛠️ (API) format & fixed eslint errors + improve types + improve error messages + add ?raw property

This commit is contained in:
pheralb
2025-09-08 12:19:15 +01:00
parent 6f7d3c51f9
commit de47f2fa03
+84 -60
View File
@@ -1,19 +1,23 @@
import { Context, Hono } from 'hono'; import type { Context } from "hono";
import { env } from 'hono/adapter'; import type { BlankInput, Env } from "hono/types";
import { cors } from 'hono/cors';
import { BlankInput, Env } from 'hono/types'; import type { iSVG } from "../../src/types/svg";
import { Ratelimit } from '@upstash/ratelimit'; import type { Category } from "../../src/types/categories";
import { Redis } from '@upstash/redis/cloudflare';
import { Hono } from "hono";
import { env } from "hono/adapter";
import { cors } from "hono/cors";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis/cloudflare";
// 🌿 Import utils: // 🌿 Import utils:
import { addFullUrl } from './utils'; import { addFullUrl } from "./utils";
import { optimizeSvg } from "../../src/utils/optimizeSvg";
// 📦 Import data from main app: // 📦 Import data from SVGL src:
import { svgsData } from '../../src/data'; import { svgsData } from "../../src/data";
import { iSVG } from '../../src/types/svg';
import { tCategory } from '../../src/types/categories';
declare module 'hono' { declare module "hono" {
interface ContextVariableMap { interface ContextVariableMap {
ratelimit: Ratelimit; ratelimit: Ratelimit;
} }
@@ -24,7 +28,7 @@ const fullRouteSvgsData = svgsData.map((svg) => {
return { return {
...svg, ...svg,
route: addFullUrl(svg.route), route: addFullUrl(svg.route),
wordmark: svg.wordmark ? addFullUrl(svg.wordmark) : undefined wordmark: svg.wordmark ? addFullUrl(svg.wordmark) : undefined,
}; };
}) as iSVG[]; }) as iSVG[];
@@ -34,21 +38,24 @@ const cache = new Map();
class RedisRateLimiter { class RedisRateLimiter {
static instance: Ratelimit; static instance: Ratelimit;
static getInstance(c: Context<Env, '/api/*', BlankInput>) { static getInstance(c: Context<Env, "/api/*", BlankInput>) {
if (!this.instance) { if (!this.instance) {
const { UPSTASH_REDIS_URL, UPSTASH_REDIS_TOKEN } = env<{ const { UPSTASH_REDIS_URL, UPSTASH_REDIS_TOKEN } = env<{
UPSTASH_REDIS_URL: string; UPSTASH_REDIS_URL: string;
UPSTASH_REDIS_TOKEN: string; UPSTASH_REDIS_TOKEN: string;
}>(c); }>(c);
const cleanRedisUrl = UPSTASH_REDIS_URL.replace(/^['"]|['"]$/g, '').trim(); const cleanRedisUrl = UPSTASH_REDIS_URL.replace(
/^['"]|['"]$/g,
"",
).trim();
const redisClient = new Redis({ const redisClient = new Redis({
token: UPSTASH_REDIS_TOKEN, token: UPSTASH_REDIS_TOKEN,
url: cleanRedisUrl url: cleanRedisUrl,
}); });
const ratelimit = new Ratelimit({ const ratelimit = new Ratelimit({
redis: redisClient, redis: redisClient,
limiter: Ratelimit.slidingWindow(5, '5 s'), limiter: Ratelimit.slidingWindow(5, "5 s"),
ephemeralCache: cache ephemeralCache: cache,
}); });
this.instance = ratelimit; this.instance = ratelimit;
return this.instance; return this.instance;
@@ -60,22 +67,22 @@ class RedisRateLimiter {
app.use(async (c, next) => { app.use(async (c, next) => {
const ratelimit = RedisRateLimiter.getInstance(c); const ratelimit = RedisRateLimiter.getInstance(c);
c.set('ratelimit', ratelimit); c.set("ratelimit", ratelimit);
await next(); await next();
}); });
app.use(cors()); app.use(cors());
// 🌱 GET: "/" - Returns all the SVGs data: // 🌱 GET: "/" - Returns all the SVGs data:
app.get('/', async (c) => { app.get("/", async (c) => {
const limit = c.req.query('limit'); const limit = c.req.query("limit");
const search = c.req.query('search'); const search = c.req.query("search");
const ratelimit = c.get('ratelimit'); const ratelimit = c.get("ratelimit");
const ip = c.req.raw.headers.get('CF-Connecting-IP'); const ip = c.req.raw.headers.get("CF-Connecting-IP");
const { success } = await ratelimit.limit(ip ?? 'anonymous'); const { success } = await ratelimit.limit(ip ?? "anonymous");
if (!success) { if (!success) {
return c.json({ error: '🛑 Too many request' }, 429); return c.json({ error: "🛑 (SVGL - API) Too many request" }, 429);
} }
if (limit) { if (limit) {
@@ -87,10 +94,10 @@ app.get('/', async (c) => {
if (search) { if (search) {
const searchResults = fullRouteSvgsData.filter((svg) => const searchResults = fullRouteSvgsData.filter((svg) =>
svg.title.toLowerCase().includes(search.toLowerCase()) svg.title.toLowerCase().includes(search.toLowerCase()),
); );
if (searchResults.length === 0) { if (searchResults.length === 0) {
return c.json({ error: 'not found' }, 404); return c.json({ error: "❌ (SVGL - API) SVG not found" }, 404);
} }
return c.json(searchResults); return c.json(searchResults);
} }
@@ -99,19 +106,19 @@ app.get('/', async (c) => {
}); });
// 🌱 GET: "/categories" - Return an array with categories: // 🌱 GET: "/categories" - Return an array with categories:
app.get('/categories', async (c) => { app.get("/categories", async (c) => {
const ratelimit = c.get('ratelimit'); const ratelimit = c.get("ratelimit");
const ip = c.req.raw.headers.get('CF-Connecting-IP'); const ip = c.req.raw.headers.get("CF-Connecting-IP");
const { success } = await ratelimit.limit(ip ?? 'anonymous'); const { success } = await ratelimit.limit(ip ?? "anonymous");
if (!success) { if (!success) {
return c.json({ error: '🛑 Too many request' }, 429); return c.json({ error: "❌ (SVGL - API) Too many request" }, 429);
} }
const categoryTotals: Record<string, number> = {}; const categoryTotals: Record<string, number> = {};
fullRouteSvgsData.forEach((svg) => { fullRouteSvgsData.forEach((svg) => {
if (typeof svg.category === 'string') { if (typeof svg.category === "string") {
categoryTotals[svg.category] = (categoryTotals[svg.category] || 0) + 1; categoryTotals[svg.category] = (categoryTotals[svg.category] || 0) + 1;
} else if (Array.isArray(svg.category)) { } else if (Array.isArray(svg.category)) {
svg.category.forEach((category) => { svg.category.forEach((category) => {
@@ -120,62 +127,79 @@ app.get('/categories', async (c) => {
} }
}); });
const categories = Object.entries(categoryTotals).map(([category, total]) => ({ const categories = Object.entries(categoryTotals).map(
category, ([category, total]) => ({
total category,
})); total,
}),
);
return c.json(categories); return c.json(categories);
}); });
// 🌱 GET: /category/:category - Return an list of svgs by specific category: // 🌱 GET: /category/:category - Return an list of svgs by specific category:
app.get('/category/:category', async (c) => { app.get("/category/:category", async (c) => {
const category = c.req.param('category') as string; const category = c.req.param("category") as string;
const targetCategory = category.charAt(0).toUpperCase() + category.slice(1); const targeCategory = category.charAt(0).toUpperCase() + category.slice(1);
const ratelimit = c.get('ratelimit'); const ratelimit = c.get("ratelimit");
const ip = c.req.raw.headers.get('CF-Connecting-IP'); const ip = c.req.raw.headers.get("CF-Connecting-IP");
const { success } = await ratelimit.limit(ip ?? 'anonymous'); const { success } = await ratelimit.limit(ip ?? "anonymous");
if (!success) { if (!success) {
return c.json({ error: '🛑 Too many request' }, 429); return c.json({ error: "🛑 (SVGL - API) Too many request" }, 429);
} }
const categorySvgs = fullRouteSvgsData.filter((svg) => { const categorySvgs = fullRouteSvgsData.filter((svg) => {
if (typeof svg.category === 'string') { if (typeof svg.category === "string") {
return svg.category === targetCategory; return svg.category === targeCategory;
} }
if (Array.isArray(svg.category)) { if (Array.isArray(svg.category)) {
return svg.category.includes(targetCategory as tCategory); return svg.category.includes(targeCategory as Category);
} }
return false; return false;
}); });
if (categorySvgs.length === 0) { if (categorySvgs.length === 0) {
return c.json({ error: 'not found' }, 404); return c.json({ error: "❌ (SVGL - API) Category not found" }, 404);
} }
return c.json(categorySvgs); return c.json(categorySvgs);
}); });
// 🌱 GET: "/svg/:filename" - Return the SVG file by filename: // 🌱 GET: "/svg/:filename" - Return the SVG code file by filename:
app.get('/svg/:filename', async (c) => { app.get("/svg/:filename", async (c) => {
const fileName = c.req.param('filename') as string; const fileName = c.req.param("filename") as string;
const svgLibrary = 'https://svgl.app/library/'; const svgLibrary = "https://svgl.app/library/";
const ratelimit = c.get('ratelimit'); const ratelimit = c.get("ratelimit");
const ip = c.req.raw.headers.get('CF-Connecting-IP'); const returnRaw = c.req.query("raw");
const { success } = await ratelimit.limit(ip ?? 'anonymous'); const ip = c.req.raw.headers.get("CF-Connecting-IP");
const { success } = await ratelimit.limit(ip ?? "anonymous");
if (!success) { if (!success) {
return c.json({ error: '🛑 Too many request' }, 429); return c.json({ error: "🛑 (SVGL - API) Too many request" }, 429);
} }
try { try {
const svg = await fetch(`${svgLibrary}${fileName}`).then((res) => { const svg = await fetch(`${svgLibrary}${fileName}`).then((res) => {
if (!res.ok) throw new Error('Network response was not ok'); if (!res.ok)
throw new Error("❌ (SVGL - API) Network response was not ok");
return res.text(); return res.text();
}); });
return c.body(svg, 200);
if (returnRaw) {
return c.body(svg, 200, {
"Content-Type": "image/svg+xml; charset=utf-8",
});
}
const optimizedSvg = optimizeSvg({ svgCode: svg });
return c.body(optimizedSvg, 200, {
"Content-Type": "image/svg+xml; charset=utf-8",
});
} catch (err) { } catch (err) {
return c.json({ error: 'not found' }, 404); return c.json(
{ error: `❌ (SVGL - API) SVG file not found - ${err}` },
404,
);
} }
}); });