diff --git a/api-routes/src/index.ts b/api-routes/src/index.ts index b4b8fe1..5732db8 100644 --- a/api-routes/src/index.ts +++ b/api-routes/src/index.ts @@ -1,19 +1,23 @@ -import { Context, Hono } from 'hono'; -import { env } from 'hono/adapter'; -import { cors } from 'hono/cors'; -import { BlankInput, Env } from 'hono/types'; -import { Ratelimit } from '@upstash/ratelimit'; -import { Redis } from '@upstash/redis/cloudflare'; +import type { Context } from "hono"; +import type { BlankInput, Env } from "hono/types"; + +import type { iSVG } from "../../src/types/svg"; +import type { Category } from "../../src/types/categories"; + +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 { addFullUrl } from './utils'; +import { addFullUrl } from "./utils"; +import { optimizeSvg } from "../../src/utils/optimizeSvg"; -// 📦 Import data from main app: -import { svgsData } from '../../src/data'; -import { iSVG } from '../../src/types/svg'; -import { tCategory } from '../../src/types/categories'; +// 📦 Import data from SVGL src: +import { svgsData } from "../../src/data"; -declare module 'hono' { +declare module "hono" { interface ContextVariableMap { ratelimit: Ratelimit; } @@ -24,7 +28,7 @@ const fullRouteSvgsData = svgsData.map((svg) => { return { ...svg, route: addFullUrl(svg.route), - wordmark: svg.wordmark ? addFullUrl(svg.wordmark) : undefined + wordmark: svg.wordmark ? addFullUrl(svg.wordmark) : undefined, }; }) as iSVG[]; @@ -34,21 +38,24 @@ const cache = new Map(); class RedisRateLimiter { static instance: Ratelimit; - static getInstance(c: Context) { + static getInstance(c: Context) { if (!this.instance) { const { UPSTASH_REDIS_URL, UPSTASH_REDIS_TOKEN } = env<{ UPSTASH_REDIS_URL: string; UPSTASH_REDIS_TOKEN: string; }>(c); - const cleanRedisUrl = UPSTASH_REDIS_URL.replace(/^['"]|['"]$/g, '').trim(); + const cleanRedisUrl = UPSTASH_REDIS_URL.replace( + /^['"]|['"]$/g, + "", + ).trim(); const redisClient = new Redis({ token: UPSTASH_REDIS_TOKEN, - url: cleanRedisUrl + url: cleanRedisUrl, }); const ratelimit = new Ratelimit({ redis: redisClient, - limiter: Ratelimit.slidingWindow(5, '5 s'), - ephemeralCache: cache + limiter: Ratelimit.slidingWindow(5, "5 s"), + ephemeralCache: cache, }); this.instance = ratelimit; return this.instance; @@ -60,22 +67,22 @@ class RedisRateLimiter { app.use(async (c, next) => { const ratelimit = RedisRateLimiter.getInstance(c); - c.set('ratelimit', ratelimit); + c.set("ratelimit", ratelimit); await next(); }); app.use(cors()); // 🌱 GET: "/" - Returns all the SVGs data: -app.get('/', async (c) => { - const limit = c.req.query('limit'); - const search = c.req.query('search'); - const ratelimit = c.get('ratelimit'); - const ip = c.req.raw.headers.get('CF-Connecting-IP'); - const { success } = await ratelimit.limit(ip ?? 'anonymous'); +app.get("/", async (c) => { + const limit = c.req.query("limit"); + const search = c.req.query("search"); + const ratelimit = c.get("ratelimit"); + const ip = c.req.raw.headers.get("CF-Connecting-IP"); + const { success } = await ratelimit.limit(ip ?? "anonymous"); if (!success) { - return c.json({ error: '🛑 Too many request' }, 429); + return c.json({ error: "🛑 (SVGL - API) Too many request" }, 429); } if (limit) { @@ -87,10 +94,10 @@ app.get('/', async (c) => { if (search) { const searchResults = fullRouteSvgsData.filter((svg) => - svg.title.toLowerCase().includes(search.toLowerCase()) + svg.title.toLowerCase().includes(search.toLowerCase()), ); 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); } @@ -99,19 +106,19 @@ app.get('/', async (c) => { }); // 🌱 GET: "/categories" - Return an array with categories: -app.get('/categories', async (c) => { - const ratelimit = c.get('ratelimit'); - const ip = c.req.raw.headers.get('CF-Connecting-IP'); - const { success } = await ratelimit.limit(ip ?? 'anonymous'); +app.get("/categories", async (c) => { + const ratelimit = c.get("ratelimit"); + const ip = c.req.raw.headers.get("CF-Connecting-IP"); + const { success } = await ratelimit.limit(ip ?? "anonymous"); if (!success) { - return c.json({ error: '🛑 Too many request' }, 429); + return c.json({ error: "❌ (SVGL - API) Too many request" }, 429); } const categoryTotals: Record = {}; fullRouteSvgsData.forEach((svg) => { - if (typeof svg.category === 'string') { + if (typeof svg.category === "string") { categoryTotals[svg.category] = (categoryTotals[svg.category] || 0) + 1; } else if (Array.isArray(svg.category)) { svg.category.forEach((category) => { @@ -120,62 +127,79 @@ app.get('/categories', async (c) => { } }); - const categories = Object.entries(categoryTotals).map(([category, total]) => ({ - category, - total - })); + const categories = Object.entries(categoryTotals).map( + ([category, total]) => ({ + category, + total, + }), + ); return c.json(categories); }); // 🌱 GET: /category/:category - Return an list of svgs by specific category: -app.get('/category/:category', async (c) => { - const category = c.req.param('category') as string; - const targetCategory = category.charAt(0).toUpperCase() + category.slice(1); - const ratelimit = c.get('ratelimit'); - const ip = c.req.raw.headers.get('CF-Connecting-IP'); - const { success } = await ratelimit.limit(ip ?? 'anonymous'); +app.get("/category/:category", async (c) => { + const category = c.req.param("category") as string; + const targeCategory = category.charAt(0).toUpperCase() + category.slice(1); + const ratelimit = c.get("ratelimit"); + const ip = c.req.raw.headers.get("CF-Connecting-IP"); + const { success } = await ratelimit.limit(ip ?? "anonymous"); 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) => { - if (typeof svg.category === 'string') { - return svg.category === targetCategory; + if (typeof svg.category === "string") { + return svg.category === targeCategory; } if (Array.isArray(svg.category)) { - return svg.category.includes(targetCategory as tCategory); + return svg.category.includes(targeCategory as Category); } return false; }); 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); }); -// 🌱 GET: "/svg/:filename" - Return the SVG file by filename: -app.get('/svg/:filename', async (c) => { - const fileName = c.req.param('filename') as string; - const svgLibrary = 'https://svgl.app/library/'; +// 🌱 GET: "/svg/:filename" - Return the SVG code file by filename: +app.get("/svg/:filename", async (c) => { + const fileName = c.req.param("filename") as string; + const svgLibrary = "https://svgl.app/library/"; - const ratelimit = c.get('ratelimit'); - const ip = c.req.raw.headers.get('CF-Connecting-IP'); - const { success } = await ratelimit.limit(ip ?? 'anonymous'); + const ratelimit = c.get("ratelimit"); + const returnRaw = c.req.query("raw"); + const ip = c.req.raw.headers.get("CF-Connecting-IP"); + const { success } = await ratelimit.limit(ip ?? "anonymous"); if (!success) { - return c.json({ error: '🛑 Too many request' }, 429); + return c.json({ error: "🛑 (SVGL - API) Too many request" }, 429); } try { 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 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) { - return c.json({ error: 'not found' }, 404); + return c.json( + { error: `❌ (SVGL - API) SVG file not found - ${err}` }, + 404, + ); } });