diff --git a/backend/src/routes/resources/main.ts b/backend/src/routes/resources/main.ts index 1d082a2..8daa5ee 100644 --- a/backend/src/routes/resources/main.ts +++ b/backend/src/routes/resources/main.ts @@ -10,30 +10,23 @@ import { or } from "drizzle-orm" - - -import {resourceConfig} from "../../utils/resource.config"; -import {useNextNumberRangeNumber} from "../../utils/functions"; -import {stafftimeentries} from "../../../db/schema"; +import { resourceConfig } from "../../utils/resource.config"; +import { useNextNumberRangeNumber } from "../../utils/functions"; // ------------------------------------------------------------- -// SQL Volltextsuche auf mehreren Feldern +// SQL Suche auf mehreren Feldern (Haupttabelle + Relationen) // ------------------------------------------------------------- - - -function buildSearchCondition(table: any, columns: string[], search: string) { +function buildSearchCondition(columns: any[], 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) } @@ -54,96 +47,85 @@ export default async function resourceRoutes(server: FastifyInstance) { asc?: string } - const {resource} = req.params as {resource: string} - const table = resourceConfig[resource].table + const { resource } = req.params as { resource: string } + const config = resourceConfig[resource] + const table = config.table - // WHERE-Basis let whereCond: any = eq(table.tenant, tenantId) + let q = server.db.select().from(table).$dynamic() - // 🔍 SQL Search - if(search) { - const searchCond = buildSearchCondition( - table, - resourceConfig[resource].searchColumns, - search.trim() - ) + const searchCols: any[] = (config.searchColumns || []).map(c => table[c]) - if (searchCond) { - whereCond = and(whereCond, searchCond) - } + 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) { + q = q.leftJoin(relTable, eq(table[rel], relTable.id)) + if (relConfig.searchColumns) { + relConfig.searchColumns.forEach(c => { + if (relTable[c]) searchCols.push(relTable[c]) + }) + } + } + } + }) } - // Base Query - let q = server.db.select().from(table).where(whereCond) + if (search) { + const searchCond = buildSearchCondition(searchCols, search.trim()) + if (searchCond) whereCond = and(whereCond, searchCond) + } + + q = q.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)) + 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 (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))]; + // 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 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])) : [] - + 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])); } - - 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 + data = rows.map(row => { + let toReturn = { ...row } + config.mtoLoad.forEach(rel => { + toReturn[rel] = row[rel] ? maps[rel][row[rel]] : 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 - }) - - + 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) + })) } } @@ -155,212 +137,129 @@ export default async function resourceRoutes(server: FastifyInstance) { } }) - // ------------------------------------------------------------- // 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" }); - } + 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 + 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(search) { - const searchCond = buildSearchCondition( - table, - resourceConfig[resource].searchColumns, - search.trim() - ) + if (config.mtoLoad) { + config.mtoLoad.forEach(rel => { + const relConfig = resourceConfig[rel + "s"] || resourceConfig[rel]; + if (relConfig) { + const relTable = relConfig.table; - if (searchCond) { - whereCond = and(whereCond, searchCond) - } + // FIX: Self-Reference Check + if (relTable !== table) { + countQuery = countQuery.leftJoin(relTable, eq(table[rel], relTable.id)); + 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; - - if (Array.isArray(val)) { - whereCond = and(whereCond, inArray(col, val)); - } else { - whereCond = and(whereCond, eq(col, val as any)); - } + whereCond = Array.isArray(val) ? and(whereCond, inArray(col, val)) : 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 totalRes = await countQuery.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"; + 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) { - orderField = col; - direction = s.direction === "asc" ? "asc" : "desc"; + mainQuery = s.direction === "asc" ? mainQuery.orderBy(asc(col)) : mainQuery.orderBy(desc(col)); } } - // 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])) : [] + 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(); } + } - resourceConfig[resource].mtoLoad.forEach(relation => { - maps[relation] = Object.fromEntries(lists[relation].map(i => [i.id, i])); - }) - + 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 - } - - resourceConfig[resource].mtoLoad.forEach(relation => { - toReturn[relation] = row[relation] ? maps[relation][row[relation]] : null - }) - - return toReturn + let toReturn = { ...row }; + config.mtoLoad.forEach(rel => { + toReturn[rel] = row[rel] ? maps[rel][row[rel]] : 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 - }) - - + 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 - // ----------------------------------------------- return { data, - queryConfig: { - ...queryConfig, - total, - totalPages: Math.ceil(total / limit), - distinctValues - } + queryConfig: { ...queryConfig, total, totalPages: Math.ceil(total / limit), distinctValues } }; } catch (err) { @@ -369,9 +268,8 @@ export default async function resourceRoutes(server: FastifyInstance) { } }); - // ------------------------------------------------------------- - // DETAIL (mit JOINS) + // DETAIL // ------------------------------------------------------------- server.get("/resource/:resource/:id/:no_relations?", async (req, reply) => { try { @@ -379,7 +277,7 @@ export default async function resourceRoutes(server: FastifyInstance) { 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 { resource, no_relations } = req.params as { resource: string, no_relations?: boolean } const table = resourceConfig[resource].table const projRows = await server.db @@ -391,40 +289,32 @@ export default async function resourceRoutes(server: FastifyInstance) { if (!projRows.length) return reply.code(404).send({ error: "Resource not found" }) - // ------------------------------------ - // LOAD RELATIONS - // ------------------------------------ + let data = { ...projRows[0] } - 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 (!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 ) { - 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)) + 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/projects/:id", err) + console.error("ERROR /resource/:resource/:id", err) return reply.code(500).send({ error: "Internal Server Error" }) } }) @@ -432,132 +322,58 @@ export default async function resourceRoutes(server: FastifyInstance) { // 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 }; + 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; - const table = resourceConfig[resource].table + let createData = { ...body, tenant: req.user.tenant_id, archived: false }; - let createData = { - ...body, - tenant: req.user.tenant_id, - archived: false, // Standardwert - } - - console.log(resourceConfig[resource].numberRangeHolder) - - if (resourceConfig[resource].numberRangeHolder && !body[resourceConfig[resource]]) { + if (config.numberRangeHolder && !body[config.numberRangeHolder]) { 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 + 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]) + 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`, - });*/ - + const [created] = await server.db.insert(table).values(createData).returning() return created; } catch (error) { - console.log(error) - reply.status(500) + console.error(error); + reply.status(500); } }); - // UPDATE (inkl. Soft-Delete/Archive) + // Update server.put("/resource/:resource/:id", async (req, reply) => { try { - const {resource, id} = req.params as { resource: string; id: string } + 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 - 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"}) - } + 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; } - //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) + let data = { ...body, updated_at: new Date().toISOString(), updated_by: userId } + delete data.updatedBy; delete data.updatedAt; Object.keys(data).forEach((key) => { - console.log(key) - - if((key.includes("_at") || key.includes("At") || key.toLowerCase().includes("date") ) && key !=="deliveryDateType") { + if ((key.includes("_at") || key.includes("At") || key.toLowerCase().includes("date")) && key !== "deliveryDateType") { 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 ?? ""}`, - }); - }*/ - + const [updated] = await server.db.update(table).set(data).where(and(eq(table.id, id), eq(table.tenant, tenantId))).returning() return updated } catch (err) { - console.log("ERROR /resource/projects/:id", err) + console.error(err) } - }) - -} +} \ No newline at end of file diff --git a/backend/src/utils/resource.config.ts b/backend/src/utils/resource.config.ts index 0384d7c..a487bad 100644 --- a/backend/src/utils/resource.config.ts +++ b/backend/src/utils/resource.config.ts @@ -36,7 +36,7 @@ import { export const resourceConfig = { projects: { - searchColumns: ["name"], + searchColumns: ["name","customerRef","projectNumber","notes"], mtoLoad: ["customer","plant","contract","projecttype"], mtmLoad: ["tasks", "files","createddocuments"], table: projects, @@ -61,6 +61,7 @@ export const resourceConfig = { }, plants: { table: plants, + searchColumns: ["name"], mtoLoad: ["customer"], mtmLoad: ["projects","tasks","files"], },