Refactor for using koa and dicebear

This commit is contained in:
Juan Sebastián Montoya 2024-09-26 01:13:22 -05:00
parent 41f480e2a6
commit 1840e5a786
17 changed files with 976 additions and 1129 deletions

View file

@ -0,0 +1,24 @@
import joi from "joi";
import { Config } from "../../types/config";
import { server } from "./server.config";
const envSchema = joi
.object({
NODE_ENV: joi
.string()
.allow("development", "production", "test")
.required(),
})
.unknown()
.required();
const { error, value: envVars } = envSchema.validate(process.env);
if (error) {
throw new Error(`Config validation error: ${error.message}`);
}
export const general: Config = {
env: envVars.NODE_ENV,
isDevelopment: envVars.NODE_ENV === "development",
server,
};

View file

@ -0,0 +1,24 @@
import joi from "joi";
import { ServerConfig } from "../../types/config";
const envSchema = joi
.object({
HOST: joi.string().default("localhost"),
PORT: joi.number().default(3000),
API_VERSION: joi.number().default(1),
ALLOWED_ORIGINS: joi.string().default("http://localhost:3000"),
})
.unknown()
.required();
const { error, value: envVars } = envSchema.validate(process.env);
if (error) {
throw new Error(`Config validation error: ${error.message}`);
}
export const server: ServerConfig = {
host: envVars.HOST,
port: envVars.PORT,
apiVersion: envVars.API_VERSION,
origins: envVars.ALLOWED_ORIGINS.split(","),
};

3
src/config/index.ts Normal file
View file

@ -0,0 +1,3 @@
import "dotenv/config";
import { general } from "./components/general.config";
export default general;

20
src/index.ts Normal file
View file

@ -0,0 +1,20 @@
import Koa from "koa";
import logger from "koa-logger";
import config from "./config";
import corsMiddleware from "./middlewares/cors.middleware";
import errorMiddleware from "./middlewares/error.middleware";
import avatarRoute from "./routes/avatar.route";
import { startServerLog } from "./utils/server";
const { server } = config;
const app = new Koa();
// Middlewares
app.use(corsMiddleware());
app.use(logger());
app.use(errorMiddleware());
app.use(avatarRoute.routes()).use(avatarRoute.allowedMethods());
app.listen(server.port, server.host, startServerLog(server.port));

View file

@ -0,0 +1,20 @@
import cors from "@koa/cors";
import { Context } from "koa";
import Router from "koa-router";
import config from "../config";
const { server } = config;
const origin = (ctx: Context) => {
const [defaultOrigin] = server.origins;
if (ctx.headers.origin && server.origins.indexOf(ctx.headers.origin) > -1) {
return ctx.header.origin || defaultOrigin;
}
return defaultOrigin;
};
const corsMiddleware: () => Router.IMiddleware<any, {}> = () =>
cors({ origin });
export default corsMiddleware;

View file

@ -0,0 +1,24 @@
import Router from "koa-router";
import { isValidationError } from "../utils/type-guards";
const errorMiddleware: () => Router.IMiddleware<any, {}> =
() => async (ctx, next) => {
try {
await next();
} catch (err) {
console.log("error middleware", err);
if (err && (err as any).status >= 500) console.log("Error handler:", err);
if (isValidationError(err)) {
ctx.status = 400;
} else {
ctx.status = (err as any).status || 500;
}
ctx.body = {
status: "failed",
message: (err as any).message || "Internal server error",
};
}
};
export default errorMiddleware;

View file

@ -0,0 +1,63 @@
import Router from "koa-router";
import { v4 as uuidv4 } from "uuid";
import { createAvatar, Style } from "@dicebear/core";
import * as openPeeps from "@dicebear/open-peeps";
import * as pixelArt from "@dicebear/pixel-art";
import * as thumbs from "@dicebear/thumbs";
import sharp from "sharp";
import { isValue, isSize, isStyle } from "../utils/type-guards";
const DENSITY = 72;
const avatars: Record<string, Style<any> & { defaultSize: number }> = {
"open-peeps": { ...openPeeps, defaultSize: 704 },
"pixel-art": { ...pixelArt, defaultSize: 16 },
thumbs: { ...thumbs, defaultSize: 100 },
};
const validStyles = Object.keys(avatars);
const isValidStyle = isStyle(validStyles);
const router = new Router();
router.get("/list", async (ctx) => {
const styles = validStyles.map((key) => ({ ...avatars[key].meta, key }));
ctx.body = { styles };
});
router.use(async (ctx, next) => {
ctx.set({
Expires: new Date(Date.now() + 604800000).toUTCString(),
});
ctx.type = "image/png";
await next();
});
router.get("/random", async (ctx) => {
const { style = "thumbs", size = "100" } = ctx.query;
if (!isValidStyle(style) || !isSize(size)) {
throw Error("Invalid query");
}
const seed = uuidv4();
const density = (DENSITY * parseInt(size)) / avatars[style].defaultSize;
ctx.body = sharp(
Buffer.from(createAvatar(avatars[style], { seed }).toString()),
{ density }
).png();
});
router.get("/:seed", async (ctx) => {
const { seed, style = "thumbs", size = "100" } = ctx.query;
if (!isValue(seed) || !isValidStyle(style) || !isSize(size)) {
throw Error("Invalid query");
}
const density = (DENSITY * parseInt(size)) / avatars[style].defaultSize;
ctx.body = sharp(
Buffer.from(createAvatar(avatars[style], { seed }).toString()),
{
density,
}
).png();
});
export default router;

12
src/types/config.ts Normal file
View file

@ -0,0 +1,12 @@
export type ServerConfig = {
host: string;
port: number;
origins: ReadonlyArray<string>;
apiVersion: number;
};
export type Config = {
env: string;
isDevelopment: boolean;
server: ServerConfig;
};

20
src/utils/server.ts Normal file
View file

@ -0,0 +1,20 @@
import { NetworkInterfaceInfo, networkInterfaces } from "os";
export const startServerLog = (port: number) => async () => {
const net = Object.values(networkInterfaces())
.flat()
.filter((v) => v?.family === "IPv4")
.sort((v) => (v!.internal ? -1 : 1)) as Array<NetworkInterfaceInfo>;
console.info("Server started successfully!");
console.info("You can now use the service.");
net.forEach(({ internal, address }) =>
console.info(
`\t${(internal ? "Local:" : "On Your Network:").padEnd(
20,
" "
)}http://${address}:${port}`
)
);
};

18
src/utils/type-guards.ts Normal file
View file

@ -0,0 +1,18 @@
import { ValidationError } from "joi";
export const isValidationError = (error: unknown): error is ValidationError =>
error instanceof ValidationError;
export const isValue = <T>(value: T | Array<T> | undefined): value is T =>
!!value && !Array.isArray(value);
export const isSize = (value: unknown): value is string =>
!Array.isArray(value) &&
!isNaN(parseInt(value as string)) &&
parseInt(value as string) <= 512 &&
parseInt(value as string) >= 16;
export const isStyle =
(validStyles: string[]) =>
<T>(value: T | Array<T> | undefined): value is T =>
!!value && !Array.isArray(value) && validStyles.includes(value as string);