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 {stafftimeentries} from "../../../db/schema"; // ------------------------------------------------------------- // 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 = [...queryData] 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 ) { console.log(relation) 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 }); } if(resourceConfig[resource].mtmListLoad) { for await (const relation of resourceConfig[resource].mtmListLoad) { console.log(relation) console.log(resource.substring(0,resource.length-1)) const relationRows = await server.db.select().from(resourceConfig[relation].table).where(inArray(resourceConfig[relation].table[resource.substring(0,resource.length-1)],data.map(i => i.id))) console.log(relationRows.length) data = data.map(row => { let toReturn = { ...row } toReturn[relation] = relationRows.filter(i => i[resource.substring(0,resource.length-1)] === row.id) return toReturn }) } } 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 } }; } let data = [...rows] //Many to One if(resourceConfig[resource].mtoLoad) { let ids = {} let lists = {} let maps = {} 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 }); } if(resourceConfig[resource].mtmListLoad) { for await (const relation of resourceConfig[resource].mtmListLoad) { console.log(relation) const relationRows = await server.db.select().from(resourceConfig[relation].table).where(inArray(resourceConfig[relation].table[resource.substring(0,resource.length-1)],data.map(i => i.id))) console.log(relationRows) data = data.map(row => { let toReturn = { ...row } toReturn[relation] = relationRows.filter(i => i[resource.substring(0,resource.length-1)] === row.id) return toReturn }) } } // ----------------------------------------------- // 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/: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" }) // ------------------------------------ // LOAD RELATIONS // ------------------------------------ let ids = {} let lists = {} let maps = {} let data = { ...projRows[0] } if(!no_relations) { 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])))[0] } } } if(resourceConfig[resource].mtmLoad) { 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" }) } }) // 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 table = resourceConfig[resource].table let createData = { ...body, tenant: req.user.tenant_id, archived: false, // Standardwert } console.log(resourceConfig[resource].numberRangeHolder) if (resourceConfig[resource].numberRangeHolder && !body[resourceConfig[resource]]) { const result = await useNextNumberRangeNumber(server, req.user.tenant_id, resource) console.log(result) createData[resourceConfig[resource].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() /*await insertHistoryItem(server, { entity: resource, entityId: data.id, action: "created", created_by: req.user.user_id, tenant_id: req.user.tenant_id, oldVal: null, newVal: data, text: `${dataType.labelSingle} erstellt`, });*/ return created; } catch (error) { console.log(error) reply.status(500) } }); // UPDATE (inkl. Soft-Delete/Archive) 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 as any)?.tenant_id const userId = (req.user as any)?.user_id if (!tenantId || !userId) { return reply.code(401).send({error: "Unauthorized"}) } const table = resourceConfig[resource].table //TODO: HISTORY 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 //@ts-ignore delete data.updatedAt console.log(data) Object.keys(data).forEach((key) => { console.log(key) if(key.includes("_at") || key.includes("At") || key.toLowerCase().includes("date")) { data[key] = normalizeDate(data[key]) } }) console.log(data) const [updated] = await server.db .update(table) .set(data) .where(and( eq(table.id, id), eq(table.tenant, tenantId))) .returning() //const diffs = diffObjects(oldItem, newItem); /*for (const d of diffs) { await insertHistoryItem(server, { entity: resource, entityId: id, action: d.type, created_by: userId, tenant_id: tenantId, oldVal: d.oldValue ? String(d.oldValue) : null, newVal: d.newValue ? String(d.newValue) : null, text: `Feld "${d.label}" ${d.typeLabel}: ${d.oldValue ?? ""} → ${d.newValue ?? ""}`, }); }*/ return updated } catch (err) { console.log("ERROR /resource/projects/:id", err) } }) }