From a07b705f80db73a2284a485fcb19cdbefe8bc5cf Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Fri, 28 Nov 2025 21:53:28 +1000 Subject: [PATCH] fix(api): optimize /api/tools by removing expensive count query --- apps/web/src/app/api/tools/route.ts | 167 +++++++++++++--------------- 1 file changed, 76 insertions(+), 91 deletions(-) diff --git a/apps/web/src/app/api/tools/route.ts b/apps/web/src/app/api/tools/route.ts index d7881da..aae68cd 100644 --- a/apps/web/src/app/api/tools/route.ts +++ b/apps/web/src/app/api/tools/route.ts @@ -1,5 +1,5 @@ -// import { type Prisma, prisma } from '@tpmjs/db'; -import { NextResponse } from 'next/server'; +import { type Prisma, prisma } from '@tpmjs/db'; +import { type NextRequest, NextResponse } from 'next/server'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -16,103 +16,88 @@ export const maxDuration = 60; * - limit: Results per page (default: 20, max: 100) * - offset: Pagination offset (default: 0) */ -export async function GET() { - // Temporary: Return simple response to test if endpoint works - return NextResponse.json({ - status: 'ok', - message: 'Tools endpoint working (database queries disabled for testing)', - timestamp: new Date().toISOString(), - env: { - hasDatabase: !!process.env.DATABASE_URL, - databaseUrlPrefix: process.env.DATABASE_URL?.substring(0, 20) + '...', // Show first 20 chars only for security - }, - }); +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; - // try { - // const searchParams = request.nextUrl.searchParams; + // Parse query parameters + const query = searchParams.get('q'); + const category = searchParams.get('category'); + const officialParam = searchParams.get('official'); + const limitParam = searchParams.get('limit'); + const offsetParam = searchParams.get('offset'); - // // Parse query parameters - // const query = searchParams.get('q'); - // const category = searchParams.get('category'); - // const officialParam = searchParams.get('official'); - // const limitParam = searchParams.get('limit'); - // const offsetParam = searchParams.get('offset'); + // Validate and set defaults (reduced max from 100 to 50 for faster queries) + const limit = Math.min( + Number.parseInt(limitParam || '20', 10), + 50 // Reduced from 100 for better performance + ); + const offset = Math.max(Number.parseInt(offsetParam || '0', 10), 0); - // // Validate and set defaults - // const limit = Math.min( - // Number.parseInt(limitParam || '20', 10), - // 100 // Max 100 results per page - // ); - // const offset = Math.max(Number.parseInt(offsetParam || '0', 10), 0); + // Build where clause + const where: Prisma.ToolWhereInput = {}; - // // Build where clause - // const where: Prisma.ToolWhereInput = {}; + // Search filter (case-insensitive partial match) + if (query) { + where.OR = [ + { npmPackageName: { contains: query, mode: 'insensitive' } }, + { description: { contains: query, mode: 'insensitive' } }, + { + tags: { + hasSome: [query], + }, + }, + ]; + } - // // Search filter (case-insensitive partial match) - // if (query) { - // where.OR = [ - // { npmPackageName: { contains: query, mode: 'insensitive' } }, - // { description: { contains: query, mode: 'insensitive' } }, - // { - // tags: { - // hasSome: [query], - // }, - // }, - // ]; - // } + // Category filter + if (category) { + where.category = category; + } - // // Category filter - // if (category) { - // where.category = category; - // } + // Official filter + if (officialParam !== null) { + where.isOfficial = officialParam === 'true'; + } - // // Official filter - // if (officialParam !== null) { - // where.isOfficial = officialParam === 'true'; - // } + // Execute queries - run count separately only if needed for pagination + // For first page, we can skip count if we don't need total pages + const tools = await prisma.tool.findMany({ + where, + orderBy: [ + { qualityScore: 'desc' }, + { npmDownloadsLastMonth: 'desc' }, + { createdAt: 'desc' }, + ], + take: limit + 1, // Fetch one extra to check if there are more + skip: offset, + }); - // // Execute query with pagination - // const [tools, totalCount] = await Promise.all([ - // prisma.tool.findMany({ - // where, - // orderBy: [ - // { qualityScore: 'desc' }, - // { npmDownloadsLastMonth: 'desc' }, - // { createdAt: 'desc' }, - // ], - // take: limit, - // skip: offset, - // }), - // prisma.tool.count({ where }), - // ]); + // Check if there are more results + const hasMore = tools.length > limit; + const actualTools = hasMore ? tools.slice(0, limit) : tools; - // // Calculate pagination metadata - // const hasMore = offset + limit < totalCount; - // const totalPages = Math.ceil(totalCount / limit); - // const currentPage = Math.floor(offset / limit) + 1; + return NextResponse.json({ + success: true, + data: actualTools, + pagination: { + limit, + offset, + hasMore, + // Note: total count omitted for performance (can be expensive) + // Only return count if explicitly requested + }, + }); + } catch (error) { + console.error('Error fetching tools:', error); - // return NextResponse.json({ - // success: true, - // data: tools, - // pagination: { - // total: totalCount, - // limit, - // offset, - // hasMore, - // totalPages, - // currentPage, - // }, - // }); - // } catch (error) { - // console.error('Error fetching tools:', error); - - // return NextResponse.json( - // { - // success: false, - // error: 'Failed to fetch tools', - // message: error instanceof Error ? error.message : 'Unknown error', - // }, - // { status: 500 } - // ); - // } + return NextResponse.json( + { + success: false, + error: 'Failed to fetch tools', + message: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ); + } }