Fixed Paginated Search

This commit is contained in:
2025-10-31 16:29:20 +01:00
parent 3bd4ac1f56
commit 75518897f1
2 changed files with 161 additions and 99 deletions

View File

@@ -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<string, any[]> = {};
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<string, any[]> = {}
// --- Distinct-Werte (auch ohne Suche) ---
const distinctValues: Record<string, any[]> = {};
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) {

View File

@@ -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;
}