Added Backend
This commit is contained in:
555
backend/src/routes/resources/main.ts
Normal file
555
backend/src/routes/resources/main.ts
Normal file
@@ -0,0 +1,555 @@
|
||||
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<string, any[]> = {};
|
||||
|
||||
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<string, any>;
|
||||
|
||||
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")) 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<string, any>
|
||||
|
||||
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}
|
||||
|
||||
Object.keys(data).forEach((key) => {
|
||||
if(key.includes("_at") || key.includes("At")) {
|
||||
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)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user