diff --git a/src/routes/resources.ts b/src/routes/resources.ts index e60eea7..8ae1840 100644 --- a/src/routes/resources.ts +++ b/src/routes/resources.ts @@ -3,6 +3,7 @@ import {insertHistoryItem} from "../utils/history" import {diffObjects} from "../utils/diff"; import {sortData} from "../utils/sort"; import {useNextNumberRangeNumber} from "../utils/functions"; +import {compareValues, getNestedValue} from "../utils/helpers"; const dataTypes: any[] = { @@ -502,153 +503,198 @@ export default async function resourceRoutes(server: FastifyInstance) { }); - // Liste Paginated + // Helper Funktionen + + +// Liste Paginated server.get("/resource/:resource/paginated", async (req, reply) => { if (!req.user?.tenant_id) { - return reply.code(400).send({error: "No tenant selected"}); + 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 { select, search, searchColumns, distinctColumns } = req.query as { + select?: string; + search?: string; + searchColumns?: string; + distinctColumns?: string; + }; - const {resource} = req.params as { resource: string } - const {queryConfig} = req + console.log(req.query); + console.log(select); - const {pagination, sort, filters, paginationDisabled} = queryConfig - const {select, search, searchColumns,distinctColumns} = req.query as { - select?: string, search?: string, searchColumns?: string, distinctColumns?: string + // --- 🔍 Suche (im Backend mit Joins) --- + if (search && search.trim().length > 0) { + // 1. Alle Daten mit Joins holen (OHNE Pagination, aber mit Filtern) + let searchQuery = server.supabase + .from(resource) + .select(select || dataTypes[resource].supabaseSelectWithInformation) + .eq("tenant", req.user.tenant_id); + + // --- Filterung anwenden --- + for (const [key, val] of Object.entries(filters || {})) { + if (Array.isArray(val)) { + searchQuery = searchQuery.in(key, val); + } else if (val === true || val === false || val === null) { + searchQuery = searchQuery.is(key, val); + } else { + searchQuery = searchQuery.eq(key, val); + } + } + + const { data: allData, error: searchError } = await searchQuery; + + if (searchError) { + server.log.error(searchError); + return reply.code(400).send({ error: searchError.message }); + } + + // 2. Im Backend nach Suchbegriff filtern + const searchTerm = search.trim().toLowerCase(); + const searchCols = searchColumns + ? searchColumns.split(",").map(c => c.trim()).filter(Boolean) + : dataTypes[resource].searchableColumns || []; + + const filteredData = (allData || []).filter(row => { + if (searchCols.length === 0) { + // Fallback: Durchsuche alle String-Felder der Hauptebene + return Object.values(row).some(val => + val?.toString().toLowerCase().includes(searchTerm) + ); + } + + return searchCols.some(col => { + const value = getNestedValue(row, col); + return value?.toString().toLowerCase().includes(searchTerm); + }); + }); + + // 3. Im Backend sortieren + let sortedData = [...filteredData]; + if (sort?.length > 0) { + sortedData.sort((a, b) => { + for (const s of sort) { + const aVal = getNestedValue(a, s.field); + const bVal = getNestedValue(b, s.field); + const comparison = compareValues(aVal, bVal); + if (comparison !== 0) { + return s.direction === "asc" ? comparison : -comparison; + } + } + return 0; + }); + } + + // 4. Im Backend paginieren + const total = sortedData.length; + const paginatedData = !paginationDisabled && pagination + ? sortedData.slice(pagination.offset, pagination.offset + pagination.limit) + : sortedData; + + // 5. Distinct Values berechnen + const distinctValues: Record = {}; + if (distinctColumns) { + const cols = distinctColumns.split(",").map(c => c.trim()).filter(Boolean); + + for (const col of cols) { + // Distinct values aus den gefilterten Daten + const values = filteredData + .map(row => getNestedValue(row, col)) + .filter(v => v !== null && v !== undefined && v !== ""); + distinctValues[col] = [...new Set(values)].sort(); + } + } + + const totalPages = !paginationDisabled && pagination?.limit + ? Math.ceil(total / pagination.limit) + : 1; + + const enrichedConfig = { + ...queryConfig, + total, + totalPages, + distinctValues, + search: search || null, + }; + + return { data: paginatedData, queryConfig: enrichedConfig }; } - console.log(queryConfig) - - // --- Supabase-Basisabfrage --- + // --- Standardabfrage (ohne Suche) --- let baseQuery = server.supabase .from(resource) - .select(select ? select : "*", {count: "exact"}) // 👈 Zählt Ergebnisse direkt mit - .eq("tenant", req.user.tenant_id) + .select(select || dataTypes[resource].supabaseSelectWithInformation, { count: "exact" }) + .eq("tenant", req.user.tenant_id); - // --- 🔍 Serverseitige Suche über angegebene Spalten --- - if (search && search.trim().length > 0) { - const cols = searchColumns - ? searchColumns.split(',').map(c => c.trim()).filter(Boolean) - : [] - - if (cols.length > 0) { - const searchValue = `%${search.trim()}%` - - // JSONB-Unterfelder umwandeln in "->>" Syntax - const formattedCols = cols.map(c => { - if (c.includes('.')) { - const [jsonField, jsonKey] = c.split('.') - return `${jsonField}->>${jsonKey}` - } - return c - }) - - // or() Query dynamisch zusammenbauen - const orConditions = formattedCols - .map(f => `${f}.ilike.${searchValue}`) - .join(',') - - baseQuery = baseQuery.or(orConditions) - } - } - - // --- Filterung (intelligente Typ-Erkennung) --- - for (const [key, val] of Object.entries(queryConfig.filters)) { + // --- Filterung --- + for (const [key, val] of Object.entries(filters || {})) { if (Array.isArray(val)) { - baseQuery = baseQuery.in(key, val) - //@ts-ignore + baseQuery = baseQuery.in(key, val); } else if (val === true || val === false || val === null) { - baseQuery = baseQuery.is(key, val) + baseQuery = baseQuery.is(key, val); } else { - baseQuery = baseQuery.eq(key, val) + baseQuery = baseQuery.eq(key, val); } } // --- Sortierung --- - if (sort.length > 0) { + if (sort?.length > 0) { for (const s of sort) { - baseQuery = baseQuery.order(s.field, {ascending: s.direction === "asc"}) + baseQuery = baseQuery.order(s.field, { ascending: s.direction === "asc" }); } } // --- Pagination --- if (!paginationDisabled && pagination) { - const {offset, limit} = pagination - baseQuery = baseQuery.range(offset, offset + limit - 1) + const { offset, limit } = pagination; + baseQuery = baseQuery.range(offset, offset + limit - 1); } - // --- Abfrage ausführen --- - const {data, error, count} = await baseQuery - + const { data, error, count } = await baseQuery; if (error) { - server.log.error(error) - return reply.code(400).send({error: error.message}) + server.log.error(error); + return reply.code(400).send({ error: error.message }); } - // --- Distinct-Werte ermitteln --- - const distinctValues: Record = {} + // --- Distinct-Werte (auch ohne Suche) --- + const distinctValues: Record = {}; if (distinctColumns) { - const cols = distinctColumns.split(",").map(c => c.trim()).filter(Boolean) + const cols = distinctColumns.split(",").map(c => c.trim()).filter(Boolean); for (const col of cols) { - const isJson = col.includes(".") - const {data: allRows, error: distinctErr} = await server.supabase + const { data: allRows, error: distinctErr } = await server.supabase .from(resource) - .select(isJson ? "*" : col) - .eq("tenant", req.user.tenant_id) + .select(col) + .eq("tenant", req.user.tenant_id); - if (distinctErr) continue + if (distinctErr) continue; const values = (allRows || []) - .map(row => { - if (isJson) { - const [jsonField, jsonKey] = col.split(".") - return row?.[jsonField]?.[jsonKey] ?? null - } - return row?.[col] ?? null - }) - .filter(v => v !== null && v !== undefined && v !== "") - distinctValues[col] = [...new Set(values)].sort() + .map((row) => row?.[col] ?? null) + .filter((v) => v !== null && v !== undefined && v !== ""); + distinctValues[col] = [...new Set(values)].sort(); } } - // --- Gesamtanzahl & Seitenberechnung --- - const total = count - const totalPages = - !paginationDisabled && pagination?.limit - ? Math.ceil(total / pagination.limit) - : 1 + const total = count || 0; + const totalPages = !paginationDisabled && pagination?.limit + ? Math.ceil(total / pagination.limit) + : 1; - // --- queryConfig erweitern --- const enrichedConfig = { ...queryConfig, total, totalPages, distinctValues, - search: search || null - } + search: search || null, + }; - return { - data: data, - queryConfig: enrichedConfig - } - - - /*const { data, error } = await server.supabase - .from(resource) - //@ts-ignore - .select(select || dataTypes[resource].supabaseSelectWithInformation) - .eq("tenant", req.user.tenant_id) - if (error) { - console.log(error) - return reply.code(400).send({ error: error.message }); - } - - const sorted =sortData(data,sort,asc === "true" ? true : false) - - return sorted;*/ + return { data, queryConfig: enrichedConfig }; }); + // Detail server.get("/resource/:resource/:id/:with_information?", async (req, reply) => { if (!req.user?.tenant_id) { diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index fdd7046..f484ae1 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -53,4 +53,20 @@ export async function findCustomerOrContactByEmailOrDomain(server:FastifyInstanc } return null +} + +export function getNestedValue(obj: any, path: string): any { + return path.split('.').reduce((acc, part) => acc?.[part], obj); +} + +export function compareValues(a: any, b: any): number { + if (a === b) return 0; + if (a == null) return 1; + if (b == null) return -1; + + if (typeof a === 'string' && typeof b === 'string') { + return a.localeCompare(b); + } + + return a < b ? -1 : 1; } \ No newline at end of file