import { FastifyInstance } from "fastify" import { eq, ilike, asc, desc, and, count, inArray, or } from "drizzle-orm" import { resourceConfig } from "../../utils/resource.config"; import { useNextNumberRangeNumber } from "../../utils/functions"; import { recalculateServicePricesForTenant } from "../../modules/service-price-recalculation.service"; // ------------------------------------------------------------- // SQL Suche auf mehreren Feldern (Haupttabelle + Relationen) // ------------------------------------------------------------- function buildSearchCondition(columns: any[], search: string) { if (!search || !columns.length) return null const term = `%${search.toLowerCase()}%` const conditions = columns .filter(Boolean) .map((col) => ilike(col, term)) if (conditions.length === 0) return null 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 config = resourceConfig[resource] const table = config.table let whereCond: any = eq(table.tenant, tenantId) let q = server.db.select().from(table).$dynamic() const searchCols: any[] = (config.searchColumns || []).map(c => table[c]) if (config.mtoLoad) { config.mtoLoad.forEach(rel => { const relConfig = resourceConfig[rel + "s"] || resourceConfig[rel] if (relConfig) { const relTable = relConfig.table // FIX: Nur joinen, wenn es keine Self-Reference ist (verhindert ERROR 42712) if (relTable !== table) { // @ts-ignore q = q.leftJoin(relTable, eq(table[rel], relTable.id)) if (relConfig.searchColumns) { relConfig.searchColumns.forEach(c => { if (relTable[c]) searchCols.push(relTable[c]) }) } } } }) } if (search) { const searchCond = buildSearchCondition(searchCols, search.trim()) if (searchCond) whereCond = and(whereCond, searchCond) } q = q.where(whereCond) if (sort) { const col = (table as any)[sort] if (col) { q = ascQuery === "true" ? q.orderBy(asc(col)) : q.orderBy(desc(col)) } } const queryData = await q // Transformation: Falls Joins genutzt wurden, das Hauptobjekt extrahieren const rows = queryData.map(r => r[resource] || r.table || r); // RELATION LOADING let data = [...rows] if(config.mtoLoad) { let ids: any = {} let lists: any = {} let maps: any = {} config.mtoLoad.forEach(rel => { ids[rel] = [...new Set(rows.map(r => r[rel]).filter(Boolean))]; }) for await (const rel of config.mtoLoad) { const relConf = resourceConfig[rel + "s"] || resourceConfig[rel]; const relTab = relConf.table lists[rel] = ids[rel].length ? await server.db.select().from(relTab).where(inArray(relTab.id, ids[rel])) : [] maps[rel] = Object.fromEntries(lists[rel].map((i: any) => [i.id, i])); } data = rows.map(row => { let toReturn = { ...row } config.mtoLoad.forEach(rel => { toReturn[rel] = row[rel] ? maps[rel][row[rel]] : null }) return toReturn }); } if(config.mtmListLoad) { for await (const relation of config.mtmListLoad) { const relTable = resourceConfig[relation].table const parentKey = resource.substring(0, resource.length - 1) const relationRows = await server.db.select().from(relTable).where(inArray(relTable[parentKey], data.map(i => i.id))) data = data.map(row => ({ ...row, [relation]: relationRows.filter(i => i[parentKey] === row.id) })) } } 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 config = resourceConfig[resource]; const table = config.table; const { queryConfig } = req; const { pagination, sort, filters } = queryConfig; const { search, distinctColumns } = req.query as { search?: string; distinctColumns?: string; }; let whereCond: any = eq(table.tenant, tenantId); const searchCols: any[] = (config.searchColumns || []).map(c => table[c]); let countQuery = server.db.select({ value: count(table.id) }).from(table).$dynamic(); let mainQuery = server.db.select().from(table).$dynamic(); if (config.mtoLoad) { config.mtoLoad.forEach(rel => { const relConfig = resourceConfig[rel + "s"] || resourceConfig[rel]; if (relConfig) { const relTable = relConfig.table; // FIX: Self-Reference Check if (relTable !== table) { countQuery = countQuery.leftJoin(relTable, eq(table[rel], relTable.id)); // @ts-ignore mainQuery = mainQuery.leftJoin(relTable, eq(table[rel], relTable.id)); if (relConfig.searchColumns) { relConfig.searchColumns.forEach(c => { if (relTable[c]) searchCols.push(relTable[c]); }); } } } }); } if (search) { const searchCond = buildSearchCondition(searchCols, 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; whereCond = Array.isArray(val) ? and(whereCond, inArray(col, val)) : and(whereCond, eq(col, val as any)); } } const totalRes = await countQuery.where(whereCond); const total = Number(totalRes[0]?.value ?? 0); const offset = pagination?.offset ?? 0; const limit = pagination?.limit ?? 100; mainQuery = mainQuery.where(whereCond).offset(offset).limit(limit); if (sort?.length > 0) { const s = sort[0]; const col = (table as any)[s.field]; if (col) { mainQuery = s.direction === "asc" ? mainQuery.orderBy(asc(col)) : mainQuery.orderBy(desc(col)); } } const rawRows = await mainQuery; // Transformation für Drizzle Joins let rows = rawRows.map(r => r[resource] || r.table || r); 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 dRows = await server.db.select({ v: col }).from(table).where(eq(table.tenant, tenantId)); distinctValues[colName] = [...new Set(dRows.map(r => r.v).filter(v => v != null && v !== ""))].sort(); } } let data = [...rows]; if (config.mtoLoad) { let ids: any = {}; let lists: any = {}; let maps: any = {}; config.mtoLoad.forEach(rel => { ids[rel] = [...new Set(rows.map(r => r[rel]).filter(Boolean))]; }); for await (const rel of config.mtoLoad) { const relConf = resourceConfig[rel + "s"] || resourceConfig[rel]; const relTab = relConf.table; lists[rel] = ids[rel].length ? await server.db.select().from(relTab).where(inArray(relTab.id, ids[rel])) : []; maps[rel] = Object.fromEntries(lists[rel].map((i: any) => [i.id, i])); } data = rows.map(row => { let toReturn = { ...row }; config.mtoLoad.forEach(rel => { toReturn[rel] = row[rel] ? maps[rel][row[rel]] : null; }); return toReturn; }); } if (config.mtmListLoad) { for await (const relation of config.mtmListLoad) { const relTable = resourceConfig[relation].table; const parentKey = resource.substring(0, resource.length - 1); const relationRows = await server.db.select().from(relTable).where(inArray(relTable[parentKey], data.map(i => i.id))); data = data.map(row => ({ ...row, [relation]: relationRows.filter(i => i[parentKey] === row.id) })); } } 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 // ------------------------------------------------------------- server.get("/resource/:resource/:id/:no_relations?", 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, no_relations } = req.params as { resource: string, no_relations?: boolean } 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" }) let data = { ...projRows[0] } if (!no_relations) { if (resourceConfig[resource].mtoLoad) { for await (const relation of resourceConfig[resource].mtoLoad) { if (data[relation]) { const relConf = resourceConfig[relation + "s"] || resourceConfig[relation]; const relTable = relConf.table const relData = await server.db.select().from(relTable).where(eq(relTable.id, data[relation])) data[relation] = relData[0] || null } } } if (resourceConfig[resource].mtmLoad) { for await (const relation of resourceConfig[resource].mtmLoad) { const relTable = resourceConfig[relation].table const parentKey = resource.substring(0, resource.length - 1) data[relation] = await server.db.select().from(relTable).where(eq(relTable[parentKey], id)) } } } return data } catch (err) { console.error("ERROR /resource/:resource/:id", err) return reply.code(500).send({ error: "Internal Server Error" }) } }) // Create server.post("/resource/:resource", async (req, reply) => { try { if (!req.user?.tenant_id) return reply.code(400).send({ error: "No tenant selected" }); const { resource } = req.params as { resource: string }; const body = req.body as Record; const config = resourceConfig[resource]; const table = config.table; let createData = { ...body, tenant: req.user.tenant_id, archived: false }; if (config.numberRangeHolder && !body[config.numberRangeHolder]) { const result = await useNextNumberRangeNumber(server, req.user.tenant_id, resource) createData[config.numberRangeHolder] = result.usedNumber } const normalizeDate = (val: any) => { const d = new Date(val); return isNaN(d.getTime()) ? null : d; } Object.keys(createData).forEach((key) => { if (key.toLowerCase().includes("date") && key !== "deliveryDateType") createData[key] = normalizeDate(createData[key]) }) const [created] = await server.db.insert(table).values(createData).returning() if (["products", "services", "hourrates"].includes(resource)) { await recalculateServicePricesForTenant(server, req.user.tenant_id, req.user?.user_id || null); } return created; } catch (error) { console.error(error); reply.status(500); } }); // Update server.put("/resource/:resource/:id", async (req, reply) => { try { const { resource, id } = req.params as { resource: string; id: string } const body = req.body as Record const tenantId = req.user?.tenant_id const userId = req.user?.user_id if (!tenantId || !userId) return reply.code(401).send({ error: "Unauthorized" }) const table = resourceConfig[resource].table const normalizeDate = (val: any) => { const d = new Date(val); return isNaN(d.getTime()) ? null : d; } let data = { ...body, updated_at: new Date().toISOString(), updated_by: userId } //@ts-ignore delete data.updatedBy; delete data.updatedAt; Object.keys(data).forEach((key) => { if ((key.includes("_at") || key.includes("At") || key.toLowerCase().includes("date")) && key !== "deliveryDateType") { data[key] = normalizeDate(data[key]) } }) const [updated] = await server.db.update(table).set(data).where(and(eq(table.id, id), eq(table.tenant, tenantId))).returning() if (["products", "services", "hourrates"].includes(resource)) { await recalculateServicePricesForTenant(server, tenantId, userId); } return updated } catch (err) { console.error(err) } }) }