=== NEW === - studio/ — MCPEngine Studio scaffold (Next.js monorepo, build plan) - docs/FACTORY-V2.md — Factory v2 architecture doc - docs/CALENDLY_MCP_BUILD_SUMMARY.md — Calendly MCP build report === UPDATED SERVERS === - fieldedge: Added jobs-tools, UI build script, main entry update - lightspeed: Updated main + server entry points - squarespace: Added collection-browser + page-manager apps - toast: Added main + server entry points === INFRA === - infra/command-center/state.json — Updated pipeline state - infra/command-center/FACTORY-V2.md — Factory v2 operator playbook
92 lines
2.7 KiB
TypeScript
92 lines
2.7 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { db, marketplaceListings } from '@mcpengine/db';
|
|
import { eq, ilike, and, desc, count, or, sql } from 'drizzle-orm';
|
|
|
|
// ── GET /api/marketplace — public listing search ────────────────────────────
|
|
|
|
export async function GET(req: NextRequest) {
|
|
try {
|
|
const url = new URL(req.url);
|
|
const query = url.searchParams.get('q') || '';
|
|
const category = url.searchParams.get('category') || '';
|
|
const official = url.searchParams.get('official');
|
|
const featured = url.searchParams.get('featured');
|
|
const page = Math.max(1, parseInt(url.searchParams.get('page') || '1', 10));
|
|
const limit = Math.min(50, Math.max(1, parseInt(url.searchParams.get('limit') || '24', 10)));
|
|
const offset = (page - 1) * limit;
|
|
|
|
// Build filter conditions — only published listings
|
|
const conditions = [eq(marketplaceListings.status, 'published')];
|
|
|
|
if (category) {
|
|
conditions.push(eq(marketplaceListings.category, category));
|
|
}
|
|
|
|
if (official === 'true') {
|
|
conditions.push(eq(marketplaceListings.isOfficial, true));
|
|
}
|
|
|
|
if (featured === 'true') {
|
|
conditions.push(eq(marketplaceListings.isFeatured, true));
|
|
}
|
|
|
|
if (query) {
|
|
conditions.push(
|
|
or(
|
|
ilike(marketplaceListings.name, `%${query}%`),
|
|
ilike(marketplaceListings.description, `%${query}%`),
|
|
ilike(marketplaceListings.slug, `%${query}%`),
|
|
)!,
|
|
);
|
|
}
|
|
|
|
const where = and(...conditions);
|
|
|
|
const [rows, totalResult] = await Promise.all([
|
|
db
|
|
.select()
|
|
.from(marketplaceListings)
|
|
.where(where)
|
|
.orderBy(
|
|
desc(marketplaceListings.isFeatured),
|
|
desc(marketplaceListings.forkCount),
|
|
desc(marketplaceListings.createdAt),
|
|
)
|
|
.limit(limit)
|
|
.offset(offset),
|
|
db
|
|
.select({ count: count() })
|
|
.from(marketplaceListings)
|
|
.where(where),
|
|
]);
|
|
|
|
const total = totalResult[0]?.count ?? 0;
|
|
|
|
// Collect distinct categories for faceting
|
|
const categories = await db
|
|
.selectDistinct({ category: marketplaceListings.category })
|
|
.from(marketplaceListings)
|
|
.where(eq(marketplaceListings.status, 'published'));
|
|
|
|
return NextResponse.json({
|
|
data: rows,
|
|
meta: {
|
|
page,
|
|
limit,
|
|
total,
|
|
totalPages: Math.ceil(total / limit),
|
|
categories: categories
|
|
.map((c) => c.category)
|
|
.filter(Boolean)
|
|
.sort(),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('[GET /api/marketplace]', error);
|
|
return NextResponse.json(
|
|
{ error: 'Internal server error' },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|