Files
tool-evaluator/artifacts/api-server/src/routes/tools.ts
T
cheffe01 b8ba53598d Add icon support for tools and improve analytics chart display
Update API, database schema, and frontend components to allow optional icons for tools, and adjust analytics chart rendering for better visibility.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 776963d0-f75d-42e2-a57b-cc36bdff8495
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 0ad0b686-eebd-46d2-8314-67e9e874e224
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0683fb79-a27c-485c-9333-5f4b288c4567/776963d0-f75d-42e2-a57b-cc36bdff8495/1p7jhzu
Replit-Helium-Checkpoint-Created: true
2026-05-25 13:28:34 +00:00

188 lines
6.1 KiB
TypeScript

import { Router, type IRouter } from "express";
import { eq, ilike, desc, sql } from "drizzle-orm";
import { db, toolsTable, ratingsTable } from "@workspace/db";
import {
ListToolsQueryParams,
CreateToolBody,
GetToolParams,
UpdateToolParams,
UpdateToolBody,
DeleteToolParams,
} from "@workspace/api-zod";
import { requireAuth } from "../middleware/auth";
const router: IRouter = Router();
function buildToolWithStats(tool: typeof toolsTable.$inferSelect, ratings: { usefulness: number; usability: number }[]) {
const ratingCount = ratings.length;
const avgUsefulness = ratingCount > 0
? ratings.reduce((s, r) => s + r.usefulness, 0) / ratingCount
: null;
const avgUsability = ratingCount > 0
? ratings.reduce((s, r) => s + r.usability, 0) / ratingCount
: null;
const avgCombined = avgUsefulness != null && avgUsability != null
? (avgUsefulness + avgUsability) / 2
: null;
return { ...tool, ratingCount, avgUsefulness, avgUsability, avgCombined };
}
router.get("/tools", async (req, res): Promise<void> => {
const parsed = ListToolsQueryParams.safeParse(req.query);
if (!parsed.success) {
res.status(400).json({ error: parsed.error.message });
return;
}
const { category, search, sort } = parsed.data;
let query = db.select().from(toolsTable).$dynamic();
if (category) {
query = query.where(eq(toolsTable.category, category));
}
if (search) {
query = query.where(ilike(toolsTable.name, `%${search}%`));
}
const tools = await query.orderBy(desc(toolsTable.createdAt));
const toolIds = tools.map((t) => t.id);
let allRatings: { toolId: number; usefulness: number; usability: number }[] = [];
if (toolIds.length > 0) {
allRatings = await db
.select({ toolId: ratingsTable.toolId, usefulness: ratingsTable.usefulness, usability: ratingsTable.usability })
.from(ratingsTable)
.where(sql`${ratingsTable.toolId} = ANY(${sql`ARRAY[${sql.join(toolIds.map(id => sql`${id}`), sql`, `)}]::int[]`})`);
}
const ratingsByTool = new Map<number, { usefulness: number; usability: number }[]>();
for (const r of allRatings) {
const arr = ratingsByTool.get(r.toolId) ?? [];
arr.push({ usefulness: r.usefulness, usability: r.usability });
ratingsByTool.set(r.toolId, arr);
}
let result = tools.map((t) => buildToolWithStats(t, ratingsByTool.get(t.id) ?? []));
if (sort === "top_rated") {
result = result.sort((a, b) => (b.avgCombined ?? 0) - (a.avgCombined ?? 0));
} else if (sort === "most_reviewed") {
result = result.sort((a, b) => b.ratingCount - a.ratingCount);
}
res.json(result);
});
router.post("/tools", requireAuth, async (req, res): Promise<void> => {
const parsed = CreateToolBody.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ error: parsed.error.message });
return;
}
const [tool] = await db.insert(toolsTable).values({
name: parsed.data.name,
description: parsed.data.description,
category: parsed.data.category,
websiteUrl: parsed.data.websiteUrl ?? null,
iconUrl: parsed.data.iconUrl ?? null,
features: parsed.data.features ?? [],
tags: parsed.data.tags ?? [],
}).returning();
res.status(201).json(tool);
});
router.get("/tools/:id", async (req, res): Promise<void> => {
const params = GetToolParams.safeParse(req.params);
if (!params.success) {
res.status(400).json({ error: params.error.message });
return;
}
const [tool] = await db.select().from(toolsTable).where(eq(toolsTable.id, params.data.id));
if (!tool) {
res.status(404).json({ error: "Tool not found" });
return;
}
const ratings = await db
.select({ usefulness: ratingsTable.usefulness, usability: ratingsTable.usability })
.from(ratingsTable)
.where(eq(ratingsTable.toolId, params.data.id));
res.json(buildToolWithStats(tool, ratings));
});
router.patch("/tools/:id", requireAuth, async (req, res): Promise<void> => {
const params = UpdateToolParams.safeParse(req.params);
if (!params.success) {
res.status(400).json({ error: params.error.message });
return;
}
const parsed = UpdateToolBody.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ error: parsed.error.message });
return;
}
const updateData: Record<string, unknown> = {};
if (parsed.data.name !== undefined) updateData.name = parsed.data.name;
if (parsed.data.description !== undefined) updateData.description = parsed.data.description;
if (parsed.data.category !== undefined) updateData.category = parsed.data.category;
if (parsed.data.websiteUrl !== undefined) updateData.websiteUrl = parsed.data.websiteUrl;
if (parsed.data.iconUrl !== undefined) updateData.iconUrl = parsed.data.iconUrl;
if (parsed.data.features !== undefined) updateData.features = parsed.data.features;
if (parsed.data.tags !== undefined) updateData.tags = parsed.data.tags;
const [tool] = await db
.update(toolsTable)
.set(updateData)
.where(eq(toolsTable.id, params.data.id))
.returning();
if (!tool) {
res.status(404).json({ error: "Tool not found" });
return;
}
res.json(tool);
});
router.delete("/tools/:id", requireAuth, async (req, res): Promise<void> => {
const params = DeleteToolParams.safeParse(req.params);
if (!params.success) {
res.status(400).json({ error: params.error.message });
return;
}
const [tool] = await db.delete(toolsTable).where(eq(toolsTable.id, params.data.id)).returning();
if (!tool) {
res.status(404).json({ error: "Tool not found" });
return;
}
res.sendStatus(204);
});
router.get("/categories", async (_req, res): Promise<void> => {
const rows = await db
.selectDistinct({ category: toolsTable.category })
.from(toolsTable)
.orderBy(toolsTable.category);
res.json(rows.map((r) => r.category));
});
router.get("/features/all", async (_req, res): Promise<void> => {
const tools = await db.select({ features: toolsTable.features }).from(toolsTable);
const featureSet = new Set<string>();
for (const t of tools) {
for (const f of t.features ?? []) {
if (f && f.trim()) featureSet.add(f.trim());
}
}
res.json([...featureSet].sort());
});
export default router;