Skip to content

Conversation

regenify
Copy link
Collaborator

@regenify regenify commented Sep 17, 2025

Description:

  • Implements a protected POST API route at /api/post-groups/ingest that accepts an array of post groups, each with nested posts.
  • Uses Zod for strict validation of incoming data structure.
  • Efficiently creates each post group and its related posts using Prisma, returning the created groups with their posts in the response.
  • Secures the endpoint with an environment variable (INGEST_SECRET) to prevent unauthorized access.
  • Designed for bulk ingestion from external classifiers or services.

Summary by CodeRabbit

  • New Features
    • Added a secure API endpoint to ingest post groups and their posts.
    • Requires a shared secret via request header; unauthorized requests are rejected.
    • Validates payloads (groups, posts, sentiments, sources, categories) and returns clear 400 errors on invalid JSON or data.
    • Supports partial success: processes valid groups while reporting per-group results.
    • Returns created groups with their posts and summaries when available.
    • Uses standard HTTP responses (400, 401, 500) for consistent error handling.

Copy link

coderabbitai bot commented Sep 17, 2025

Walkthrough

Adds a Next.js POST API route at src/app/api/post-groups/ingest/route.ts that authenticates via x-ingest-secret, validates an array of post groups and posts with Zod, inserts PostGroup records and optional posts into Prisma (using create and createMany), and returns per-group results or HTTP errors.

Changes

Cohort / File(s) Summary
API Ingestion Route
src/app/api/post-groups/ingest/route.ts
New POST handler: checks x-ingest-secret vs INGEST_SECRET (401 on mismatch), parses JSON, validates payload (Zod) for groups/posts (400 on invalid JSON or schema), creates PostGroup records and bulk-inserts posts (createMany, normalizes dates), fetches created groups with posts, accumulates per-group results, and returns 200 or 500 on DB errors.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant C as Client
    participant R as Next.js Route\nPOST /api/post-groups/ingest
    participant V as Zod Validator
    participant DB as Prisma / Database

    C->>R: POST body (groups) + x-ingest-secret
    R->>R: Check process.env.INGEST_SECRET
    alt Missing/invalid secret
        R-->>C: 401 Unauthorized
    else Secret valid
        R->>R: Parse JSON
        alt Parse error
            R-->>C: 400 Bad Request
        else Parsed
            R->>V: Validate groups/posts schema
            alt Validation fail
                R-->>C: 400 Validation details
            else Valid
                loop For each group
                    R->>DB: create PostGroup (title, summaries, totalposts)
                    alt posts exist
                        R->>DB: createMany(posts mapped, dates normalized)
                    end
                    R->>DB: fetch created PostGroup with posts
                    R-->>R: append { success: true, postGroup }
                end
                R-->>C: 200 Results array
            end
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A nibble of data, a hop through the gate,
I thump the header — secret check, validate.
Groups snugly burrowed, posts placed with care,
Zod peeks, Prisma plants rows everywhere.
Ingest complete — carrot crunch, I share. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly and concisely describes the primary change: adding a bulk ingest POST API endpoint for post groups and their posts, which matches the PR summary and the new route at src/app/api/post-groups/ingest/route.ts.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/api-endpoint-post-groups

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6aaa60f and 2690b17.

📒 Files selected for processing (1)
  • src/app/api/post-groups/ingest/route.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/api/post-groups/ingest/route.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (7)
src/app/api/post-groups/ingest/route.ts (7)

1-1: Harden secret check (timing‑safe compare) and treat missing secret as 500.

Prevents subtle timing leaks and surfaces server misconfiguration distinctly from client auth failures.

+import { timingSafeEqual } from "node:crypto";
@@
-const INGEST_SECRET = process.env.INGEST_SECRET;
+// (read per request below to avoid stale values across reloads)
@@
-    if (!INGEST_SECRET || authHeader !== INGEST_SECRET) {
-        return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
-    }
+    const secret = process.env.INGEST_SECRET;
+    const provided = authHeader ?? "";
+    if (!secret) {
+        return NextResponse.json({ error: "Server misconfigured: INGEST_SECRET not set" }, { status: 500 });
+    }
+    const a = Buffer.from(provided);
+    const b = Buffer.from(secret);
+    const ok = a.length === b.length && timingSafeEqual(a, b);
+    if (!ok) {
+        return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+    }

Also applies to: 6-6, 9-13


24-33: Tighten validation (min/url/Date coercion), default posts to [], and drop redundant array check.

Avoids empty titles, validates URLs, ensures valid Dates before DB, and removes the manual non‑empty array guard in favor of schema min(1).

@@
-    const postSchema = z.object({
-        content: z.string(),
+    const postSchema = z.object({
+        content: z.string().trim().min(1),
         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(),
+        categories: z.array(z.string()).default([]),
+        subcategories: z.array(z.string()).default([]),
+        link: z.string().url().optional(),
+        createdAt: z.coerce.date().optional(),
+        updatedAt: z.coerce.date().optional(),
     });
@@
-    const postGroupSchema = z.object({
-        title: z.string(),
+    const postGroupSchema = z.object({
+        title: z.string().trim().min(1),
         bullishSummary: z.string().optional(),
         bearishSummary: z.string().optional(),
         neutralSummary: z.string().optional(),
-        posts: z.array(postSchema).optional(),
+        posts: z.array(postSchema).default([]),
     });
@@
-    const postGroupsSchema = z.array(postGroupSchema);
+    const postGroupsSchema = z.array(postGroupSchema).min(1);
@@
-    // 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 });
-    }
+    // (Redundant after schema .min(1))
@@
-                    data: group.posts.map((post: {
+                    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;
+                        createdAt?: Date;
+                        updatedAt?: Date;
                     }) => ({
@@
-                        createdAt: post.createdAt ? new Date(post.createdAt) : undefined,
-                        updatedAt: post.updatedAt ? new Date(post.updatedAt) : undefined,
+                        createdAt: post.createdAt,
+                        updatedAt: post.updatedAt,
                     })),

Also applies to: 35-43, 51-55, 78-87, 95-96


4-6: Pin runtime to Node.js to avoid accidental Edge deployment.

Prisma requires Node APIs; this prevents accidental Edge runtime selection.

+
+export const runtime = "nodejs";

24-33: Optional: derive enums from Prisma to keep Zod in lockstep with DB.

Prevents drift if enum members change in the schema.

// import { Sentiment, Source } from "@prisma/client";
// const postSchema = z.object({
//   ...
//   sentiment: z.nativeEnum(Sentiment),
//   source: z.nativeEnum(Source),
//   ...
// });

76-99: Optional: chunk large createMany batches to avoid exceeding parameter limits.

For very large groups, chunk posts (e.g., 500–1000) to keep queries safe.

// helper
async function createPostsInBatches(tx: typeof prisma, posts: any[], postGroupId: string, batch = 500) {
  for (let i = 0; i < posts.length; i += batch) {
    const slice = posts.slice(i, i + batch).map(p => ({ ...p, postGroupId }));
    await tx.post.createMany({ data: slice });
  }
}

8-13: Operational: add rate limiting and idempotency for external callers.

Reduce abuse and duplicate ingestion with:

  • Rate limit keyed by IP or service token.
  • Optional Idempotency-Key header + unique constraint (e.g., on link) with skipDuplicates.

68-68: Prisma field 'totalposts' (lowercase) matches the code; consider renaming to camelCase.

schema.prisma — PostGroup (line 55): totalposts Int @default(0)
src/app/api/post-groups/ingest/route.ts (line 68): totalposts: Array.isArray(group.posts) ? group.posts.length : 0

No runtime error expected; to rename to totalPosts run a Prisma migration and update all usages.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6827963 and 89839e1.

📒 Files selected for processing (1)
  • src/app/api/post-groups/ingest/route.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/app/api/post-groups/ingest/route.ts (1)
src/lib/prisma.ts (1)
  • prisma (13-14)
🔇 Additional comments (1)
src/app/api/post-groups/ingest/route.ts (1)

8-14: LGTM: route skeleton and early auth gate are clean.

Good use of App Router POST handler, early return on auth failure, and JSON parse guard.

@tasin2610 tasin2610 self-requested a review September 17, 2025 14:53
@tasin2610 tasin2610 merged commit 0535dbb into main Sep 17, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants