Skip to content

Commit

Permalink
Feat/stratagems (#7)
Browse files Browse the repository at this point in the history
* chore(types): add type definitions for stratagem feature

* feat: add stratagem database entities

* lib(assets): add stratagem json file

* fix: properly define activation and cooldown properties

* feat: add routes and controllers for stratagems

* feat: implement stratagem generation

* refractor: arrange static assets by type

* lib(assets): add stratagem assets

* lib(middleware): add custom middleware for static file serving

* feat: implement file serving middleware

* refractor: add base url for stratagem file serving
  • Loading branch information
fabio-nettis authored Mar 19, 2024
1 parent c5d45bb commit b0536d1
Show file tree
Hide file tree
Showing 71 changed files with 2,202 additions and 19 deletions.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ WORKDIR /usr/src/app
ENV WAR_ID="801"
ENV RATE_LIMIT="200"
ENV DATABASE_URL="file:./database/data.db"
ENV STRATAGEM_IMAGE_URL="/api/static/images/stratagems"
ENV API_URL="https://api.live.prod.thehelldiversgame.com/api"

# install dependencies into temp directory
Expand Down
23 changes: 23 additions & 0 deletions prisma/migrations/20240319124513_stratagems/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- CreateTable
CREATE TABLE "StratagemGroup" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);

-- CreateTable
CREATE TABLE "Stratagem" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"codename" TEXT,
"name" TEXT NOT NULL,
"keys" TEXT NOT NULL,
"uses" TEXT NOT NULL,
"cooldown" INTEGER,
"activation" INTEGER,
"imageUrl" TEXT NOT NULL,
"groupId" INTEGER,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Stratagem_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "StratagemGroup" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
23 changes: 23 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,26 @@ model Attack {
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
}

model StratagemGroup {
id Int @id @default(autoincrement())
name String
stratagems Stratagem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Stratagem {
id Int @id @default(autoincrement())
codename String?
name String
keys String
uses String
cooldown Int?
activation Int?
imageUrl String
group StratagemGroup? @relation(fields: [groupId], references: [id])
groupId Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
83 changes: 83 additions & 0 deletions src/controllers/stratagems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { Context } from "hono";
import { PrismaClient } from "@prisma/client";

import witCache from "utils/cache";
import parseIntParam from "utils/params";
import parseQueryParams from "utils/query";

const prisma = new PrismaClient();

export const getStratagemById = await witCache(async (ctx: Context) => {
try {
const id = parseIntParam(ctx, "id");
const query = await parseQueryParams(ctx);

delete query.orderBy;
delete query.where;
delete query.orderBy;
delete (query as any).skip;
delete (query as any).take;

const stratagem = await prisma.stratagem.findUnique({
...(query as any),
where: { id },
});

if (!stratagem) {
ctx.status(404);
return ctx.json({
data: null,
error: { details: [`Stratagem with id (${id}) not found`] },
});
}

// slightly transform the data
const imageBaseUrl = process.env.STRATAGEM_IMAGE_URL || "";
stratagem.imageUrl = `${imageBaseUrl}${stratagem.imageUrl}`;
(stratagem as any).keys = stratagem.keys.split(",");

return ctx.json({ data: stratagem, error: null });
} catch (error: any) {
console.error(error);
ctx.status(500);
return ctx.json({
data: null,
error: { details: [error.message] },
});
}
});

export const getAllStratagems = await witCache(async (ctx: Context) => {
try {
const query = await parseQueryParams(ctx);

const [count, stratagems] = await Promise.all([
prisma.stratagem.count({ where: query.where }),
prisma.stratagem.findMany(query),
]);

return ctx.json({
data: stratagems.map(stratagem => {
// slightly transform the data
const imageBaseUrl = process.env.STRATAGEM_IMAGE_URL || "";
stratagem.imageUrl = `${imageBaseUrl}${stratagem.imageUrl}`;
(stratagem as any).keys = stratagem.keys.split(",");
return stratagem;
}),
error: null,
pagination: {
page: query.skip / query.take + 1,
pageSize: query.take,
pageCount: Math.ceil((count as number) / query.take),
total: count,
},
});
} catch (error: any) {
console.error(error);
ctx.status(500);
return ctx.json({
data: null,
error: { details: [error.message] },
});
}
});
18 changes: 16 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import "jobs/refresh";

import { Hono } from "hono";

import files from "middleware/files";
import cache from "middleware/cache";
import rateLimit from "middleware/rate-limit";

Expand All @@ -12,14 +13,27 @@ import planets from "routes/planets";
import sectors from "routes/sectors";
import attacks from "routes/attacks";
import factions from "routes/factions";
import stratagems from "routes/stratagems";

// initiate hono api
const app = new Hono().basePath("/api");

// middleware for the api
app.use("/static/*", files);
app.use(rateLimit);
app.use(cache);

// routes for the api
const routes = [planets, sectors, wars, factions, attacks, events, orders];
for (const route of routes) await route(app);
const routes = [
planets,
sectors,
wars,
factions,
attacks,
events,
orders,
stratagems,
];

for (const route of routes) await route(app);
export default app;
97 changes: 97 additions & 0 deletions src/middleware/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import path from "path";
import type { Context, Next } from "hono";

const getMimeType = (filename: string) => {
const mimes = {
aac: "audio/aac",
avi: "video/x-msvideo",
avif: "image/avif",
av1: "video/av1",
bin: "application/octet-stream",
bmp: "image/bmp",
css: "text/css",
csv: "text/csv",
eot: "application/vnd.ms-fontobject",
epub: "application/epub+zip",
gif: "image/gif",
gz: "application/gzip",
htm: "text/html",
html: "text/html",
ico: "image/x-icon",
ics: "text/calendar",
jpeg: "image/jpeg",
jpg: "image/jpeg",
js: "text/javascript",
json: "application/json",
jsonld: "application/ld+json",
map: "application/json",
mid: "audio/x-midi",
midi: "audio/x-midi",
mjs: "text/javascript",
mp3: "audio/mpeg",
mp4: "video/mp4",
mpeg: "video/mpeg",
oga: "audio/ogg",
ogv: "video/ogg",
ogx: "application/ogg",
opus: "audio/opus",
otf: "font/otf",
pdf: "application/pdf",
png: "image/png",
rtf: "application/rtf",
svg: "image/svg+xml",
tif: "image/tiff",
tiff: "image/tiff",
ts: "video/mp2t",
ttf: "font/ttf",
txt: "text/plain",
wasm: "application/wasm",
webm: "video/webm",
weba: "audio/webm",
webp: "image/webp",
woff: "font/woff",
woff2: "font/woff2",
xhtml: "application/xhtml+xml",
xml: "application/xml",
zip: "application/zip",
"3gp": "video/3gpp",
"3g2": "video/3gpp2",
gltf: "model/gltf+json",
glb: "model/gltf-binary",
};

const regexp = /\.([a-zA-Z0-9]+?)$/;
const match = filename.match(regexp);
if (!match) return;
let mimeType = mimes[match[1] as keyof typeof mimes];
if (
(mimeType && mimeType.startsWith("text")) ||
mimeType === "application/json"
) {
mimeType += "; charset=utf-8";
}
return mimeType;
};

export default async function files(ctx: Context, next: Next) {
try {
const _ctx = {
...ctx,
req: { ...ctx.req, url: ctx.req.url.replace("/api", "") },
};

const { pathname } = new URL(_ctx.req.url);
if (!pathname.startsWith("/static/")) await next();

const file = Bun.file(path.join(process.cwd(), "src", pathname));
if (!file) await next();

const mimeType = getMimeType(`..${pathname}`);
if (!mimeType) return await next();

const content = await file.arrayBuffer();
return ctx.newResponse(content, 200, { "Content-Type": mimeType });
} catch {
return ctx.newResponse("Not Found", 404);
}
}
9 changes: 9 additions & 0 deletions src/routes/stratagems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Hono } from "hono";

import * as Stratagems from "controllers/stratagems";

export default async function stratagems(app: Hono) {
app.get("/stratagems", Stratagems.getAllStratagems);
app.get("/stratagems/:id", Stratagems.getStratagemById);
}
//https://api-helldivers-companion.koyeb.app
24 changes: 24 additions & 0 deletions src/static/images/stratagems/1/1.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions src/static/images/stratagems/1/2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions src/static/images/stratagems/1/3.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions src/static/images/stratagems/1/4.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit b0536d1

Please sign in to comment.