Files
FEDEO/backend/src/routes/resources/main.ts
florianfederspiel 8c2a8a7998
Some checks failed
Build and Push Docker Images / build-backend (push) Failing after 32s
Build and Push Docker Images / build-frontend (push) Has been cancelled
Fix #60
2026-02-15 13:17:56 +01:00

394 lines
16 KiB
TypeScript

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<string, any[]> = {};
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<string, any>;
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<string, any>
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)
}
})
}