mirror of
https://github.com/pheralb/svgl.git
synced 2025-12-29 08:01:36 +08:00
🛠️ (API) format & fixed eslint errors + improve types + improve error messages + add ?raw property
This commit is contained in:
+83
-59
@@ -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, total]) => ({
|
||||||
category,
|
category,
|
||||||
total
|
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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user