/opt/canhelp/apps/api/src/routes
Edit: /opt/canhelp/apps/api/src/routes/categories.ts (1867B)
import { Hono } from 'hono'
import { eq, isNull, asc } from 'drizzle-orm'
import { db } from '../db.js'
import { categories } from '@canhelp/db'
import { redis } from '../redis.js'
const CACHE_KEY = 'categories:tree:v2'
const CACHE_TTL = 600 // 10 min
const app = new Hono()
function rowToCategory(row: typeof categories.$inferSelect) {
return {
id: row.id,
slug: row.slug,
icon: row.icon,
names: { el: row.namesEl, en: row.namesEn, ru: row.namesRu, uk: row.namesUk ?? row.namesRu },
parentId: row.parentId,
order: row.order,
isActive: row.isActive,
}
}
// Build tree from flat list
function buildTree(rows: typeof categories.$inferSelect[]) {
const map: Record
= {}
const roots: any[] = []
for (const row of rows) {
map[row.id] = { ...rowToCategory(row), children: [] }
}
for (const row of rows) {
if (row.parentId && map[row.parentId]) {
map[row.parentId].children.push(map[row.id])
} else {
roots.push(map[row.id])
}
}
return roots
}
// GET /categories — returns tree of active categories
app.get('/', async (c) => {
const cached = await redis.get(CACHE_KEY).catch(() => null)
if (cached) return c.json(JSON.parse(cached))
const rows = await db
.select()
.from(categories)
.where(eq(categories.isActive, true))
.orderBy(asc(categories.order), asc(categories.namesEl))
const tree = buildTree(rows)
await redis.set(CACHE_KEY, JSON.stringify(tree), 'EX', CACHE_TTL).catch(() => {})
return c.json(tree)
})
// GET /categories/flat — flat list for dropdowns (includes inactive for admin)
app.get('/flat', async (c) => {
const rows = await db
.select()
.from(categories)
.orderBy(asc(categories.order), asc(categories.namesEl))
return c.json(rows.map(rowToCategory))
})
export { buildTree, rowToCategory }
export default app