From 8ff63fadec20a65e8aec4d590b985306ada2bf36 Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Mon, 27 Oct 2025 17:40:25 +0100 Subject: [PATCH] Added Paginated Endpoint Reformatted Code --- src/index.ts | 16 ++++- src/plugins/queryconfig.ts | 125 +++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 src/plugins/queryconfig.ts diff --git a/src/index.ts b/src/index.ts index 0040ea7..7a435d9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import authRoutesAuthenticated from "./routes/auth/auth-authenticated"; import authPlugin from "./plugins/auth"; import adminRoutes from "./routes/admin"; import corsPlugin from "./plugins/cors"; +import queryConfigPlugin from "./plugins/queryconfig"; import resourceRoutes from "./routes/resources"; import resourceRoutesSpecial from "./routes/resourcesSpecial"; import fastifyCookie from "@fastify/cookie"; @@ -20,6 +21,7 @@ import functionRoutes from "./routes/functions"; import bankingRoutes from "./routes/banking"; import exportRoutes from "./routes/exports" import emailAsUserRoutes from "./routes/emailAsUser"; +import authProfilesRoutes from "./routes/profiles"; import {sendMail} from "./utils/mailer"; import {loadSecrets, secrets} from "./utils/secrets"; @@ -27,7 +29,7 @@ import {initMailer} from "./utils/mailer" import {initS3} from "./utils/s3"; async function main() { - const app = Fastify({ logger: true }); + const app = Fastify({ logger: false }); await loadSecrets(); await initMailer(); await initS3(); @@ -42,6 +44,17 @@ async function main() { await app.register(corsPlugin); await app.register(supabasePlugin); await app.register(tenantPlugin); + + app.addHook('preHandler', (req, reply, done) => { + console.log('Matched path:', req.routeOptions.url) + done() + }) + + //Plugin nur auf bestimmten Routes + await app.register(queryConfigPlugin, { + routes: ['/api/resource/:resource/paginated'] + }) + app.register(fastifyCookie, { secret: secrets.COOKIE_SECRET, }) @@ -68,6 +81,7 @@ async function main() { await subApp.register(bankingRoutes); await subApp.register(exportRoutes); await subApp.register(emailAsUserRoutes); + await subApp.register(authProfilesRoutes); },{prefix: "/api"}) diff --git a/src/plugins/queryconfig.ts b/src/plugins/queryconfig.ts new file mode 100644 index 0000000..3ecb8c0 --- /dev/null +++ b/src/plugins/queryconfig.ts @@ -0,0 +1,125 @@ +import fp from 'fastify-plugin' +import { FastifyPluginAsync, FastifyRequest } from 'fastify' + +export interface QueryConfigPagination { + page: number + limit: number + offset: number +} + +export interface QueryConfigSort { + field: string + direction: 'asc' | 'desc' +} + +export interface QueryConfig { + pagination: QueryConfigPagination | null + sort: QueryConfigSort[] + filters: Record + paginationDisabled: boolean +} + +declare module 'fastify' { + interface FastifyRequest { + queryConfig: QueryConfig + } +} + +interface QueryConfigPluginOptions { + routes?: string[] +} + +function matchRoutePattern(currentPath: string, patterns: string[]): boolean { + return patterns.some(pattern => { + // Beispiel: /users/:id -> /^\/users\/[^/]+$/ + const regex = new RegExp( + '^' + + pattern + .replace(/\*/g, '.*') // wildcard + .replace(/:[^/]+/g, '[^/]+') + + '$' + ) + return regex.test(currentPath) + }) +} + +const queryConfigPlugin: FastifyPluginAsync = async ( + fastify, + opts +) => { + const routePatterns = opts.routes || [] + + fastify.addHook('preHandler', async (req: FastifyRequest, reply) => { + const path = req.routeOptions.url || req.raw.url || '' + + if (!matchRoutePattern(path, routePatterns)) { + return + } + + const query = req.query as Record + + console.log(query) + + // Pagination deaktivieren? + const disablePagination = + query.noPagination === 'true' || + query.pagination === 'false' || + query.limit === '0' + + // Pagination berechnen + let pagination: QueryConfigPagination | null = null + if (!disablePagination) { + const page = Math.max(parseInt(query.page) || 1, 1) + const limit = Math.max(parseInt(query.limit) || 25, 1) + const offset = (page - 1) * limit + pagination = { page, limit, offset } + } + + // Sortierung + const sort: QueryConfigSort[] = [] + if (typeof query.sort === 'string') { + const items = query.sort.split(',') + for (const item of items) { + const [field, direction] = item.split(':') + sort.push({ + field: field.trim(), + direction: (direction || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc' + }) + } + } + + // Filterung + const filters: Record = {} + + for (const [key, value] of Object.entries(query)) { + const match = key.match(/^filter\[(.+)\]$/) + if (!match) continue + + const filterKey = match[1] + + if (typeof value === 'string') { + // Split bei Komma → mehrere Werte + const parts = value.split(',').map(v => v.trim()).filter(Boolean) + + // Automatische Typkonvertierung je Element + const parsedValues = parts.map(v => { + if (v === 'true') return true + if (v === 'false') return false + if (v === 'null') return null + return v + }) + + filters[filterKey] = parsedValues.length > 1 ? parsedValues : parsedValues[0] + } + } + + req.queryConfig = { + pagination, + sort, + filters, + paginationDisabled: disablePagination + } + }) +} + +export default fp(queryConfigPlugin, { name: 'query-config' })