import { FastifyInstance } from "fastify" import { eq, ilike, asc, desc, and, count, inArray, or } from "drizzle-orm" import {resourceConfig} from "../../resource.config"; // ------------------------------------------------------------- // SQL Volltextsuche auf mehreren Feldern // ------------------------------------------------------------- function buildSearchCondition(table: any, columns: string[], search: string) { if (!search || !columns.length) return null const term = `%${search.toLowerCase()}%` const conditions = columns .map((colName) => table[colName]) .filter(Boolean) .map((col) => ilike(col, term)) if (conditions.length === 0) return null // @ts-ignore return or(...conditions) } export default async function resourceRoutes(server: FastifyInstance) { // ------------------------------------------------------------- // LIST // ------------------------------------------------------------- server.get("/resource/:resource", async (req, reply) => { try { const tenantId = req.user?.tenant_id if (!tenantId) return reply.code(400).send({ error: "No tenant selected" }) const { search, sort, asc: ascQuery } = req.query as { search?: string sort?: string asc?: string } const {resource} = req.params as {resource: string} const table = resourceConfig[resource].table // WHERE-Basis let whereCond: any = eq(table.tenant, tenantId) // 🔍 SQL Search if(search) { const searchCond = buildSearchCondition( table, resourceConfig[resource].searchColumns, search.trim() ) if (searchCond) { whereCond = and(whereCond, searchCond) } } // Base Query let q = server.db.select().from(table).where(whereCond) // Sortierung if (sort) { const col = (table as any)[sort] if (col) { //@ts-ignore q = ascQuery === "true" ? q.orderBy(asc(col)) : q.orderBy(desc(col)) } } const queryData = await q // RELATION LOADING (MANY-TO-ONE) let ids = {} let lists = {} let maps = {} let data = [] if(resourceConfig[resource].mtoLoad) { resourceConfig[resource].mtoLoad.forEach(relation => { ids[relation] = [...new Set(queryData.map(r => r[relation]).filter(Boolean))]; }) for await (const relation of resourceConfig[resource].mtoLoad ) { lists[relation] = ids[relation].length ? await server.db.select().from(resourceConfig[relation + "s"].table).where(inArray(resourceConfig[relation + "s"].table.id, ids[relation])) : [] } resourceConfig[resource].mtoLoad.forEach(relation => { maps[relation] = Object.fromEntries(lists[relation].map(i => [i.id, i])); }) data = queryData.map(row => { let toReturn = { ...row } resourceConfig[resource].mtoLoad.forEach(relation => { toReturn[relation] = row[relation] ? maps[relation][row[relation]] : null }) return toReturn }); } else { data = queryData } return data } catch (err) { console.error("ERROR /resource/:resource", err) return reply.code(500).send({ error: "Internal Server Error" }) } }) // ------------------------------------------------------------- // PAGINATED LIST // ------------------------------------------------------------- server.get("/resource/:resource/paginated", async (req, reply) => { try { const tenantId = req.user?.tenant_id; if (!tenantId) { return reply.code(400).send({ error: "No tenant selected" }); } const {resource} = req.params as {resource: string}; const {queryConfig} = req; const { pagination, sort, filters, paginationDisabled } = queryConfig; const { search, distinctColumns } = req.query as { search?: string; distinctColumns?: string; }; let table = resourceConfig[resource].table let whereCond: any = eq(table.tenant, tenantId); if(search) { const searchCond = buildSearchCondition( table, resourceConfig[resource].searchColumns, search.trim() ) if (searchCond) { whereCond = and(whereCond, searchCond) } } if (filters) { for (const [key, val] of Object.entries(filters)) { const col = (table as any)[key]; if (!col) continue; if (Array.isArray(val)) { whereCond = and(whereCond, inArray(col, val)); } else { whereCond = and(whereCond, eq(col, val as any)); } } } // ----------------------------------------------- // COUNT (for pagination) // ----------------------------------------------- const totalRes = await server.db .select({ value: count(table.id) }) .from(table) .where(whereCond); const total = Number(totalRes[0]?.value ?? 0); // ----------------------------------------------- // DISTINCT VALUES (regardless of pagination) // ----------------------------------------------- const distinctValues: Record = {}; if (distinctColumns) { for (const colName of distinctColumns.split(",").map(c => c.trim())) { const col = (table as any)[colName]; if (!col) continue; const rows = await server.db .select({ v: col }) .from(table) .where(eq(table.tenant, tenantId)); const values = rows .map(r => r.v) .filter(v => v != null && v !== ""); distinctValues[colName] = [...new Set(values)].sort(); } } // PAGINATION const offset = pagination?.offset ?? 0; const limit = pagination?.limit ?? 100; // SORTING let orderField: any = null; let direction: "asc" | "desc" = "asc"; if (sort?.length > 0) { const s = sort[0]; const col = (table as any)[s.field]; if (col) { orderField = col; direction = s.direction === "asc" ? "asc" : "desc"; } } // MAIN QUERY (Paginated) let q = server.db .select() .from(table) .where(whereCond) .offset(offset) .limit(limit); if (orderField) { //@ts-ignore q = direction === "asc" ? q.orderBy(asc(orderField)) : q.orderBy(desc(orderField)); } const rows = await q; if (!rows.length) { return { data: [], queryConfig: { ...queryConfig, total, totalPages: 0, distinctValues } }; } // RELATION LOADING (MANY-TO-ONE) let ids = {} let lists = {} let maps = {} let data = [] if(resourceConfig[resource].mtoLoad) { resourceConfig[resource].mtoLoad.forEach(relation => { ids[relation] = [...new Set(rows.map(r => r[relation]).filter(Boolean))]; }) for await (const relation of resourceConfig[resource].mtoLoad ) { lists[relation] = ids[relation].length ? await server.db.select().from(resourceConfig[relation + "s"].table).where(inArray(resourceConfig[relation + "s"].table.id, ids[relation])) : [] } resourceConfig[resource].mtoLoad.forEach(relation => { maps[relation] = Object.fromEntries(lists[relation].map(i => [i.id, i])); }) data = rows.map(row => { let toReturn = { ...row } resourceConfig[resource].mtoLoad.forEach(relation => { toReturn[relation] = row[relation] ? maps[relation][row[relation]] : null }) return toReturn }); } else { data = rows } // ----------------------------------------------- // RETURN DATA // ----------------------------------------------- return { data, queryConfig: { ...queryConfig, total, totalPages: Math.ceil(total / limit), distinctValues } }; } catch (err) { console.error(`ERROR /resource/:resource/paginated:`, err); return reply.code(500).send({ error: "Internal Server Error" }); } }); // ------------------------------------------------------------- // DETAIL (mit JOINS) // ------------------------------------------------------------- server.get("/resource/:resource/:id", async (req, reply) => { try { const { id } = req.params as { id: string } const tenantId = req.user?.tenant_id if (!tenantId) return reply.code(400).send({ error: "No tenant selected" }) const {resource} = req.params as { resource: string } const table = resourceConfig[resource].table const projRows = await server.db .select() .from(table) .where(and(eq(table.id, id), eq(table.tenant, tenantId))) .limit(1) if (!projRows.length) return reply.code(404).send({ error: "Resource not found" }) // ------------------------------------ // LOAD RELATIONS // ------------------------------------ let ids = {} let lists = {} let maps = {} let data = { ...projRows[0] } if(resourceConfig[resource].mtoLoad) { for await (const relation of resourceConfig[resource].mtoLoad ) { if(data[relation]) { data[relation] = await server.db.select().from(resourceConfig[relation + "s"].table).where(eq(resourceConfig[relation + "s"].table.id, data[relation])) } } for await (const relation of resourceConfig[resource].mtmLoad ) { console.log(relation) data[relation] = await server.db.select().from(resourceConfig[relation].table).where(eq(resourceConfig[relation].table[resource.substring(0,resource.length - 1)],id)) } } return data } catch (err) { console.error("ERROR /resource/projects/:id", err) return reply.code(500).send({ error: "Internal Server Error" }) } }) }