=== 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
121 lines
3.5 KiB
TypeScript
121 lines
3.5 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { auth } from '@clerk/nextjs/server';
|
|
import { db, projects, users } from '@mcpengine/db';
|
|
import { eq, desc, and, count } from 'drizzle-orm';
|
|
|
|
// ── GET /api/projects — list user's projects with pagination ────────────────
|
|
|
|
export async function GET(req: NextRequest) {
|
|
try {
|
|
const { userId: clerkId } = await auth();
|
|
if (!clerkId) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
}
|
|
|
|
const user = await db.query.users.findFirst({
|
|
where: eq(users.clerkId, clerkId),
|
|
});
|
|
if (!user) {
|
|
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
|
}
|
|
|
|
const url = new URL(req.url);
|
|
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') || '20', 10)));
|
|
const offset = (page - 1) * limit;
|
|
|
|
const [rows, totalResult] = await Promise.all([
|
|
db.query.projects.findMany({
|
|
where: eq(projects.userId, user.id),
|
|
orderBy: [desc(projects.updatedAt)],
|
|
limit,
|
|
offset,
|
|
with: {
|
|
tools: { columns: { id: true, name: true, enabled: true } },
|
|
apps: { columns: { id: true, name: true, pattern: true } },
|
|
},
|
|
}),
|
|
db.select({ count: count() }).from(projects).where(eq(projects.userId, user.id)),
|
|
]);
|
|
|
|
const total = totalResult[0]?.count ?? 0;
|
|
|
|
return NextResponse.json({
|
|
data: rows,
|
|
meta: {
|
|
page,
|
|
limit,
|
|
total,
|
|
totalPages: Math.ceil(total / limit),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('[GET /api/projects]', error);
|
|
return NextResponse.json(
|
|
{ error: 'Internal server error' },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── POST /api/projects — create a new project ──────────────────────────────
|
|
|
|
export async function POST(req: NextRequest) {
|
|
try {
|
|
const { userId: clerkId } = await auth();
|
|
if (!clerkId) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
}
|
|
|
|
const user = await db.query.users.findFirst({
|
|
where: eq(users.clerkId, clerkId),
|
|
});
|
|
if (!user) {
|
|
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
|
}
|
|
|
|
const body = await req.json();
|
|
const { name, slug, description, specUrl, templateId } = body;
|
|
|
|
if (!name || !slug) {
|
|
return NextResponse.json(
|
|
{ error: 'name and slug are required' },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Check for duplicate slug under same user
|
|
const existing = await db.query.projects.findFirst({
|
|
where: and(eq(projects.userId, user.id), eq(projects.slug, slug)),
|
|
});
|
|
if (existing) {
|
|
return NextResponse.json(
|
|
{ error: 'A project with this slug already exists' },
|
|
{ status: 409 },
|
|
);
|
|
}
|
|
|
|
const [project] = (await db
|
|
.insert(projects)
|
|
.values({
|
|
userId: user.id,
|
|
teamId: user.teamId,
|
|
name,
|
|
slug,
|
|
description: description || null,
|
|
specUrl: specUrl || null,
|
|
templateId: templateId || null,
|
|
status: 'draft',
|
|
})
|
|
.returning()) as any[];
|
|
|
|
return NextResponse.json({ data: project }, { status: 201 });
|
|
} catch (error) {
|
|
console.error('[POST /api/projects]', error);
|
|
return NextResponse.json(
|
|
{ error: 'Internal server error' },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|