ref: dir structure of backend package

This commit is contained in:
alikia2x (寒寒) 2025-05-10 00:20:20 +08:00
parent bf00918c00
commit 01f5e57864
Signed by: alikia2x
GPG Key ID: 56209E0CCD8420C6
23 changed files with 103 additions and 101 deletions

View File

@ -35,7 +35,7 @@ export const postgresCredConfigNpm = {
database: databaseNameCred,
username: databaseUser,
password: databasePassword
}
};
export const postgresConfigCred = {
hostname: databaseHost,

View File

@ -2,4 +2,4 @@ import postgres from "postgres";
import { postgresConfigNpm, postgresCredConfigNpm } from "./config";
export const sql = postgres(postgresConfigNpm);
export const sqlCred = postgres(postgresCredConfigNpm)
export const sqlCred = postgres(postgresCredConfigNpm);

View File

@ -3,4 +3,4 @@ import { Context, Next } from "hono";
export const contentType = async (c: Context, next: Next) => {
await next();
c.header("Content-Type", "application/json; charset=utf-8");
};
};

View File

@ -8,7 +8,7 @@ import { logger } from "./logger.ts";
import { timing } from "hono/timing";
import { contentType } from "./contentType.ts";
export function configureMiddleWares(app: Hono<{Variables: Variables }>) {
export function configureMiddleWares(app: Hono<{ Variables: Variables }>) {
app.use("*", contentType);
app.use(timing());
app.use("*", preetifyResponse);
@ -16,4 +16,4 @@ export function configureMiddleWares(app: Hono<{Variables: Variables }>) {
app.post("/user", registerRateLimiter);
app.all("/ping", bodyLimitForPing, ...pingHandler);
}
}

View File

@ -77,7 +77,7 @@ const defaultFormatter = (params) => {
`${methodColor} ${params.method.padEnd(6)}${reset} ${params.path}`
);
};
type Ctx = Context
type Ctx = Context;
export const logger = (config) => {
const { formatter = defaultFormatter, output = console, skipPaths = [], skip = null } = config;

View File

@ -15,4 +15,4 @@ export const preetifyResponse = async (c: Context, next: Next) => {
endTime(c, "seralize");
c.res = new Response(prettyJson, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
}
};
};

View File

@ -24,4 +24,4 @@ export const registerRateLimiter = rateLimiter<BlankEnv, "/user", {}>({
// @ts-expect-error - Known issue: the `c`all` function is not present in @types/ioredis
sendCommand: (...args: string[]) => redis.call(...args)
}) as unknown as Store
});
});

View File

@ -7,4 +7,4 @@ export const notFoundRoute = (c: Context) => {
},
404
);
};
};

View File

@ -1,17 +1,29 @@
import { rootHandler } from "./root/root.ts";
import { pingHandler } from "./ping.ts";
import { getSnapshotsHanlder } from "./snapshots.ts";
import { registerHandler } from "./user.ts";
import { videoInfoHandler } from "db/videoInfo.ts";
import { Hono } from "hono";
import { Variables } from "hono/types";
import { getSingerForBirthday, pickSinger, pickSpecialSinger, type Singer } from "lib/const/singers.ts";
import { VERSION } from "src/main.ts";
import { createHandlers } from "src/utils.ts";
export function configureRoutes(app: Hono<{Variables: Variables }>) {
app.get("/", ...rootHandler);
app.all("/ping", ...pingHandler);
app.get("/video/:id/snapshots", ...getSnapshotsHanlder);
app.post("/user", ...registerHandler);
app.get("/video/:id/info", ...videoInfoHandler);
}
export const rootHandler = createHandlers((c) => {
let singer: Singer | Singer[];
const shouldShowSpecialSinger = Math.random() < 0.016;
if (getSingerForBirthday().length !== 0) {
singer = JSON.parse(JSON.stringify(getSingerForBirthday())) as Singer[];
for (const s of singer) {
delete s.birthday;
s.message = `${s.name}生日快乐~`;
}
} else if (shouldShowSpecialSinger) {
singer = pickSpecialSinger();
} else {
singer = pickSinger();
}
return c.json({
project: {
name: "中V档案馆",
motto: "一起唱吧,心中的歌!"
},
status: 200,
version: VERSION,
time: Date.now(),
singer: singer
});
});

View File

@ -1,24 +0,0 @@
import { getClientIP } from "middleware/logger.ts";
import { createHandlers } from "../src/utils.ts";
import { VERSION } from "../src/main.ts";
export const pingHandler = createHandlers(async (c) => {
const requestHeaders = c.req.raw.headers;
return c.json({
"message": "pong",
"request": {
"headers": requestHeaders,
"ip": getClientIP(c),
"mode": c.req.raw.mode,
"method": c.req.method,
"query": new URL(c.req.url).searchParams,
"body": await c.req.text(),
"url": c.req.raw.url
},
"response": {
"time": new Date().getTime(),
"status": 200,
"version": VERSION,
}
});
});

View File

@ -0,0 +1,24 @@
import { getClientIP } from "middleware/logger.ts";
import { createHandlers } from "src/utils.ts";
import { VERSION } from "src/main.ts";
export const pingHandler = createHandlers(async (c) => {
const requestHeaders = c.req.raw.headers;
return c.json({
message: "pong",
request: {
headers: requestHeaders,
ip: getClientIP(c),
mode: c.req.raw.mode,
method: c.req.method,
query: new URL(c.req.url).searchParams,
body: await c.req.text(),
url: c.req.raw.url
},
response: {
time: new Date().getTime(),
status: 200,
version: VERSION
}
});
});

View File

@ -1,29 +0,0 @@
import { getSingerForBirthday, pickSinger, pickSpecialSinger, type Singer } from "./singers.ts";
import { VERSION } from "../../src/main.ts";
import { createHandlers } from "../../src/utils.ts";
export const rootHandler = createHandlers((c) => {
let singer: Singer | Singer[];
const shouldShowSpecialSinger = Math.random() < 0.016;
if (getSingerForBirthday().length !== 0) {
singer = JSON.parse(JSON.stringify(getSingerForBirthday())) as Singer[];
for (const s of singer) {
delete s.birthday;
s.message = `${s.name}生日快乐~`;
}
} else if (shouldShowSpecialSinger) {
singer = pickSpecialSinger();
} else {
singer = pickSinger();
}
return c.json({
project: {
name: "中V档案馆",
motto: "一起唱吧,心中的歌!"
},
status: 200,
version: VERSION,
time: Date.now(),
singer: singer
});
});

View File

@ -0,0 +1 @@
export * from "./register.ts";

View File

@ -44,7 +44,7 @@ export const registerHandler = createHandlers(async (c: ContextType) => {
const response: StatusResponse = {
message: `User '${username}' registered successfully.`
}
};
return c.json<StatusResponse>(response, 201);
} catch (e) {
@ -53,21 +53,21 @@ export const registerHandler = createHandlers(async (c: ContextType) => {
message: "Invalid registration data.",
errors: e.errors,
code: "INVALID_PAYLOAD"
}
};
return c.json<ErrorResponse<string>>(response, 400);
} else if (e instanceof SyntaxError) {
const response: ErrorResponse<string> = {
message: "Invalid JSON payload.",
errors: [e.message],
code: "INVALID_FORMAT"
}
};
return c.json<ErrorResponse<string>>(response, 400);
} else {
const response: ErrorResponse<string> = {
message: "Invalid JSON payload.",
errors: [(e as Error).message],
code: "UNKNOWN_ERR"
}
};
return c.json<ErrorResponse<string>>(response, 500);
}
}

View File

@ -1,15 +1,15 @@
import logger from "@core/log/logger.ts";
import { redis } from "@core/db/redis.ts";
import { sql } from "./db.ts";
import { sql } from "../../../db/db.ts";
import { number, ValidationError } from "yup";
import { createHandlers } from "../src/utils.ts";
import { createHandlers } from "../../../src/utils.ts";
import { getVideoInfo, getVideoInfoByBV } from "@core/net/getVideoInfo.ts";
import { idSchema } from "../routes/snapshots.ts";
import { idSchema } from "./snapshots.ts";
import { NetSchedulerError } from "@core/net/delegate.ts";
import type { Context } from "hono";
import type { BlankEnv, BlankInput } from "hono/types";
import type { VideoInfoData } from "@core/net/bilibili.d.ts";
import { startTime, endTime } from 'hono/timing'
import { startTime, endTime } from "hono/timing";
const CACHE_EXPIRATION_SECONDS = 60;
@ -34,7 +34,7 @@ async function insertVideoSnapshot(data: VideoInfoData) {
}
export const videoInfoHandler = createHandlers(async (c: ContextType) => {
startTime(c, 'parse', 'Parse the request');
startTime(c, "parse", "Parse the request");
try {
const id = await idSchema.validate(c.req.param("id"));
let videoId: string | number = id as string;
@ -45,33 +45,33 @@ export const videoInfoHandler = createHandlers(async (c: ContextType) => {
}
const cacheKey = `cvsa:videoInfo:${videoId}`;
endTime(c, 'parse');
startTime(c, 'cache', 'Check for cached data');
endTime(c, "parse");
startTime(c, "cache", "Check for cached data");
const cachedData = await redis.get(cacheKey);
endTime(c, 'cache');
endTime(c, "cache");
if (cachedData) {
return c.json(JSON.parse(cachedData));
}
startTime(c, 'net', 'Fetch data');
startTime(c, "net", "Fetch data");
let result: VideoInfoData | number;
if (typeof videoId === "number") {
result = await getVideoInfo(videoId, "getVideoInfo");
} else {
result = await getVideoInfoByBV(videoId, "getVideoInfo");
}
endTime(c, 'net');
endTime(c, "net");
if (typeof result === "number") {
return c.json({ message: "Error fetching video info", code: result }, 500);
}
startTime(c, 'db', 'Write data to database');
startTime(c, "db", "Write data to database");
await redis.setex(cacheKey, CACHE_EXPIRATION_SECONDS, JSON.stringify(result));
await insertVideoSnapshot(result);
endTime(c, 'db');
endTime(c, "db");
return c.json(result);
} catch (e) {
if (e instanceof ValidationError) {

View File

@ -1,10 +1,10 @@
import type { Context } from "hono";
import { createHandlers } from "../src/utils.ts";
import { createHandlers } from "src/utils.ts";
import type { BlankEnv, BlankInput } from "hono/types";
import { getVideoSnapshots, getVideoSnapshotsByBV } from "../db/videoSnapshot.ts";
import { getVideoSnapshots, getVideoSnapshotsByBV } from "db/snapshots.ts";
import type { VideoSnapshotType } from "@core/db/schema.d.ts";
import { boolean, mixed, number, object, ValidationError } from "yup";
import { ErrorResponse } from "../src/schema";
import { ErrorResponse } from "src/schema";
import { startTime, endTime } from "hono/timing";
const SnapshotQueryParamsSchema = object({

View File

@ -0,0 +1,2 @@
export * from "./[id]/info";
export * from "./[id]/snapshots";

View File

@ -1,7 +1,7 @@
import { Hono } from "hono";
import type { TimingVariables } from "hono/timing";
import { startServer } from "./startServer.ts";
import { configureRoutes } from "routes";
import { configureRoutes } from "./routing.ts";
import { configureMiddleWares } from "middleware";
import { notFoundRoute } from "routes/404.ts";

View File

@ -0,0 +1,16 @@
import { rootHandler } from "routes";
import { pingHandler } from "routes/ping";
import { registerHandler } from "routes/user";
import { videoInfoHandler, getSnapshotsHanlder } from "routes/video";
import { Hono } from "hono";
import { Variables } from "hono/types";
export function configureRoutes(app: Hono<{ Variables: Variables }>) {
app.get("/", ...rootHandler);
app.all("/ping", ...pingHandler);
app.get("/video/:id/snapshots", ...getSnapshotsHanlder);
app.post("/user", ...registerHandler);
app.get("/video/:id/info", ...videoInfoHandler);
}

View File

@ -1,11 +1,11 @@
type ErrorCode = "INVALID_QUERY_PARAMS" | "UNKNOWN_ERR" | "INVALID_PAYLOAD" | "INVALID_FORMAT" | "BODY_TOO_LARGE";
export interface ErrorResponse<E> {
code: ErrorCode
code: ErrorCode;
message: string;
errors: E[];
}
export interface StatusResponse {
message: string;
}
}

View File

@ -32,7 +32,7 @@ function logStartup(hostname: string, port: number, wasAutoIncremented: boolean,
console.log("\nPress Ctrl+C to quit.");
}
export async function startServer(app: Hono<{Variables: Variables }>) {
export async function startServer(app: Hono<{ Variables: Variables }>) {
const NODE_ENV = process.env.NODE_ENV || "production";
const HOST = process.env.HOST ?? (NODE_ENV === "development" ? "0.0.0.0" : "127.0.0.1");
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;