From 89839e1244333d1648907f495658330abd283089 Mon Sep 17 00:00:00 2001 From: Ronald Gene Date: Wed, 17 Sep 2025 07:02:37 -0600 Subject: [PATCH 1/4] feat: Implement POST endpoint for ingesting post groups with validation and database integration --- src/app/api/post-groups/ingest/route.ts | 113 ++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/app/api/post-groups/ingest/route.ts diff --git a/src/app/api/post-groups/ingest/route.ts b/src/app/api/post-groups/ingest/route.ts new file mode 100644 index 0000000..ed50939 --- /dev/null +++ b/src/app/api/post-groups/ingest/route.ts @@ -0,0 +1,113 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { z } from "zod"; + +// Set this env variable in your deployment environment +const INGEST_SECRET = process.env.INGEST_SECRET; + +export async function POST(req: NextRequest) { + const authHeader = req.headers.get("x-ingest-secret"); + + if (!INGEST_SECRET || authHeader !== INGEST_SECRET) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + + let data; + try { + data = await req.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); + } + + // Zod validation schema + const postSchema = z.object({ + content: z.string(), + sentiment: z.enum(["BULLISH", "NEUTRAL", "BEARISH"]), + source: z.enum(["REDDIT", "TWITTER", "YOUTUBE", "TELEGRAM", "FARCASTER"]), + categories: z.array(z.string()), + subcategories: z.array(z.string()), + link: z.string().optional(), + createdAt: z.string().datetime().optional(), + updatedAt: z.string().datetime().optional(), + }); + + const postGroupSchema = z.object({ + title: z.string(), + bullishSummary: z.string().optional(), + bearishSummary: z.string().optional(), + neutralSummary: z.string().optional(), + posts: z.array(postSchema).optional(), + }); + + const postGroupsSchema = z.array(postGroupSchema); + + const parseResult = postGroupsSchema.safeParse(data); + if (!parseResult.success) { + return NextResponse.json({ error: "Validation failed", details: parseResult.error.flatten() }, { status: 400 }); + } + data = parseResult.data; + + // Accept an array of post groups + if (!Array.isArray(data) || data.length === 0) { + return NextResponse.json({ error: "Request body must be a non-empty array of post groups" }, { status: 400 }); + } + + const results = []; + try { + for (const group of data) { + if (!group.title) { + results.push({ error: "Missing title in post group", group }); + continue; + } + + // Create the post group + const createdGroup = await prisma.postGroup.create({ + data: { + title: group.title, + totalposts: Array.isArray(group.posts) ? group.posts.length : 0, + bullishSummary: group.bullishSummary, + bearishSummary: group.bearishSummary, + neutralSummary: group.neutralSummary, + }, + }); + + // Create posts for this group using createMany + if (Array.isArray(group.posts) && group.posts.length > 0) { + await prisma.post.createMany({ + data: group.posts.map((post: { + content: string; + sentiment: "BULLISH" | "NEUTRAL" | "BEARISH"; + source: "REDDIT" | "TWITTER" | "YOUTUBE" | "TELEGRAM" | "FARCASTER"; + categories: string[]; + subcategories: string[]; + link?: string; + createdAt?: string; + updatedAt?: string; + }) => ({ + content: post.content, + sentiment: post.sentiment, + source: post.source, + categories: post.categories, + subcategories: post.subcategories, + link: post.link, + postGroupId: createdGroup.id, + createdAt: post.createdAt ? new Date(post.createdAt) : undefined, + updatedAt: post.updatedAt ? new Date(post.updatedAt) : undefined, + })), + }); + } + + // Fetch the group with its posts + const groupWithPosts = await prisma.postGroup.findUnique({ + where: { id: createdGroup.id }, + include: { posts: true }, + }); + + results.push({ success: true, postGroup: groupWithPosts }); + } + return NextResponse.json(results); + } catch (e) { + return NextResponse.json({ error: "Database error", details: String(e) }, { status: 500 }); + } +} \ No newline at end of file From bda91a949934798bc063af8023d07cbc0631d5a2 Mon Sep 17 00:00:00 2001 From: Ronald Gene Date: Wed, 17 Sep 2025 07:19:59 -0600 Subject: [PATCH 2/4] refactor: Clean up post group ingestion logic and improve error handling --- src/app/api/post-groups/ingest/route.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/app/api/post-groups/ingest/route.ts b/src/app/api/post-groups/ingest/route.ts index ed50939..07b953b 100644 --- a/src/app/api/post-groups/ingest/route.ts +++ b/src/app/api/post-groups/ingest/route.ts @@ -12,7 +12,6 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - let data; try { data = await req.json(); @@ -48,19 +47,10 @@ export async function POST(req: NextRequest) { } data = parseResult.data; - // Accept an array of post groups - if (!Array.isArray(data) || data.length === 0) { - return NextResponse.json({ error: "Request body must be a non-empty array of post groups" }, { status: 400 }); - } const results = []; try { for (const group of data) { - if (!group.title) { - results.push({ error: "Missing title in post group", group }); - continue; - } - // Create the post group const createdGroup = await prisma.postGroup.create({ data: { @@ -108,6 +98,7 @@ export async function POST(req: NextRequest) { } return NextResponse.json(results); } catch (e) { - return NextResponse.json({ error: "Database error", details: String(e) }, { status: 500 }); + console.error("Database error during post-group ingest", e); + return NextResponse.json({ error: "Database error" }, { status: 500 }); } } \ No newline at end of file From 6aaa60fb29cbbf0444a49fef9efc733996e14f5d Mon Sep 17 00:00:00 2001 From: Ronald Gene Date: Wed, 17 Sep 2025 07:47:34 -0600 Subject: [PATCH 3/4] feat: Refactor post group ingestion to improve validation and database creation logic --- src/app/api/post-groups/ingest/route.ts | 102 +++++++++++------------- 1 file changed, 47 insertions(+), 55 deletions(-) diff --git a/src/app/api/post-groups/ingest/route.ts b/src/app/api/post-groups/ingest/route.ts index 07b953b..a878f8c 100644 --- a/src/app/api/post-groups/ingest/route.ts +++ b/src/app/api/post-groups/ingest/route.ts @@ -1,10 +1,40 @@ import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; +import { Sentiment, Source } from "@prisma/client"; import { z } from "zod"; // Set this env variable in your deployment environment const INGEST_SECRET = process.env.INGEST_SECRET; +// Zod validation schema (moved outside handler for efficiency) +const postSchema = z.object({ + content: z.string(), + sentiment: z.enum([Sentiment.BULLISH, Sentiment.NEUTRAL, Sentiment.BEARISH]), + source: z.enum([ + Source.REDDIT, + Source.TWITTER, + Source.YOUTUBE, + Source.TELEGRAM, + Source.FARCASTER, + ]), + categories: z.array(z.string()), + subcategories: z.array(z.string()), + link: z.string().optional(), + createdAt: z.string().datetime().optional(), + updatedAt: z.string().datetime().optional(), +}); + +const postGroupSchema = z.object({ + title: z.string(), + bullishSummary: z.string().optional(), + bearishSummary: z.string().optional(), + neutralSummary: z.string().optional(), + posts: z.array(postSchema), +}); + +const postGroupsSchema = z.array(postGroupSchema); + + export async function POST(req: NextRequest) { const authHeader = req.headers.get("x-ingest-secret"); @@ -19,27 +49,6 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); } - // Zod validation schema - const postSchema = z.object({ - content: z.string(), - sentiment: z.enum(["BULLISH", "NEUTRAL", "BEARISH"]), - source: z.enum(["REDDIT", "TWITTER", "YOUTUBE", "TELEGRAM", "FARCASTER"]), - categories: z.array(z.string()), - subcategories: z.array(z.string()), - link: z.string().optional(), - createdAt: z.string().datetime().optional(), - updatedAt: z.string().datetime().optional(), - }); - - const postGroupSchema = z.object({ - title: z.string(), - bullishSummary: z.string().optional(), - bearishSummary: z.string().optional(), - neutralSummary: z.string().optional(), - posts: z.array(postSchema).optional(), - }); - - const postGroupsSchema = z.array(postGroupSchema); const parseResult = postGroupsSchema.safeParse(data); if (!parseResult.success) { @@ -51,50 +60,33 @@ export async function POST(req: NextRequest) { const results = []; try { for (const group of data) { - // Create the post group + // Create the post group with all its posts atomically const createdGroup = await prisma.postGroup.create({ data: { title: group.title, - totalposts: Array.isArray(group.posts) ? group.posts.length : 0, + totalposts: group.posts.length, bullishSummary: group.bullishSummary, bearishSummary: group.bearishSummary, neutralSummary: group.neutralSummary, + posts: { + createMany: { + data: group.posts.map(post => ({ + content: post.content, + sentiment: post.sentiment, + source: post.source, + categories: post.categories, + subcategories: post.subcategories, + link: post.link, + createdAt: post.createdAt ? new Date(post.createdAt) : undefined, + updatedAt: post.updatedAt ? new Date(post.updatedAt) : undefined, + })), + }, + }, }, - }); - - // Create posts for this group using createMany - if (Array.isArray(group.posts) && group.posts.length > 0) { - await prisma.post.createMany({ - data: group.posts.map((post: { - content: string; - sentiment: "BULLISH" | "NEUTRAL" | "BEARISH"; - source: "REDDIT" | "TWITTER" | "YOUTUBE" | "TELEGRAM" | "FARCASTER"; - categories: string[]; - subcategories: string[]; - link?: string; - createdAt?: string; - updatedAt?: string; - }) => ({ - content: post.content, - sentiment: post.sentiment, - source: post.source, - categories: post.categories, - subcategories: post.subcategories, - link: post.link, - postGroupId: createdGroup.id, - createdAt: post.createdAt ? new Date(post.createdAt) : undefined, - updatedAt: post.updatedAt ? new Date(post.updatedAt) : undefined, - })), - }); - } - - // Fetch the group with its posts - const groupWithPosts = await prisma.postGroup.findUnique({ - where: { id: createdGroup.id }, include: { posts: true }, }); - results.push({ success: true, postGroup: groupWithPosts }); + results.push({ success: true, postGroup: createdGroup }); } return NextResponse.json(results); } catch (e) { From 2690b1754002fc54edf475d45e0b19dcbb12d726 Mon Sep 17 00:00:00 2001 From: Ronald Gene Date: Wed, 17 Sep 2025 07:53:50 -0600 Subject: [PATCH 4/4] refactor: Update validation schemas to use native enums for sentiment and source --- src/app/api/post-groups/ingest/route.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/app/api/post-groups/ingest/route.ts b/src/app/api/post-groups/ingest/route.ts index a878f8c..919e3b4 100644 --- a/src/app/api/post-groups/ingest/route.ts +++ b/src/app/api/post-groups/ingest/route.ts @@ -9,14 +9,8 @@ const INGEST_SECRET = process.env.INGEST_SECRET; // Zod validation schema (moved outside handler for efficiency) const postSchema = z.object({ content: z.string(), - sentiment: z.enum([Sentiment.BULLISH, Sentiment.NEUTRAL, Sentiment.BEARISH]), - source: z.enum([ - Source.REDDIT, - Source.TWITTER, - Source.YOUTUBE, - Source.TELEGRAM, - Source.FARCASTER, - ]), + sentiment: z.nativeEnum(Sentiment), + source: z.nativeEnum(Source), categories: z.array(z.string()), subcategories: z.array(z.string()), link: z.string().optional(),