Add support for auto-generating socials#4
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR adds a full social media auto-generation pipeline: content ingestion via "dumps", AI-powered classification and post drafting (OpenAI), image/video generation (OpenAI + OpenRouter), weekly planning, and publishing through Postiz. The implementation spans new DB tables, 9 Inngest background functions, and ~20 API routes. Several issues need attention before merging:
Confidence Score: 2/5Not safe to merge — overly permissive RLS allows cross-tenant data access, and multiple P1 logic bugs affect the core automated pipeline. Multiple P1 findings: the RLS cross-tenant security issue, the dead guard in socialWeeklyPlanner, the item double-assignment race in draft-one, and unguarded JSON parsing in two AI helpers that can stall the pipeline. migrations/20260427_socials.sql (RLS policies), apps/autogtm/src/inngest/functions.ts (dead guard), packages/autogtm-core/src/ai/classifySocialItems.ts and draftSocialPost.ts (unguarded JSON.parse), apps/autogtm/src/app/api/companies/[id]/social/themes/[themeId]/draft-one/route.ts (item reservation race).
|
| Filename | Overview |
|---|---|
| migrations/20260427_socials.sql | Adds 7 new social tables; RLS policies only check auth.role() = 'authenticated' with no company-level scoping, and used_for_post_id lacks a FK constraint. |
| apps/autogtm/src/inngest/functions.ts | Adds 9 Inngest functions for the social pipeline; the socialWeeklyPlanner guard (allItems.length > 5000) can never trigger because the query uses limit: 1. |
| packages/autogtm-core/src/ai/classifySocialItems.ts | AI classification helper; unguarded JSON.parse on model output will throw on malformed responses, leaving dumps stuck in processing state. |
| packages/autogtm-core/src/ai/draftSocialPost.ts | Drafts social post copy via OpenAI; same unguarded JSON.parse issue that can leave posts stranded in planned status on model errors. |
| apps/autogtm/src/app/api/companies/[id]/social/themes/[themeId]/draft-one/route.ts | Creates a post and fires async Inngest draft event but does not mark the source data item as reserved, leaving it eligible for re-use by concurrent requests or the weekly planner. |
| apps/autogtm/src/app/api/companies/[id]/social/week-plans/route.ts | Week-plan generation endpoint; contains an inline dynamic createClient import that bypasses the shared getServiceSupabase() helper. |
| apps/autogtm/src/app/api/companies/[id]/social/_lib.ts | Shared helpers for the social API routes; creates Supabase admin client directly with createClient rather than using the project's canonical helper. |
| packages/autogtm-core/src/db/socialsDbCalls.ts | Comprehensive DB layer for social entities; all writes scope by company_id, covering themes, schedules, week plans, posts, and publish runs. |
| packages/autogtm-core/src/ai/generateSocialImage.ts | Generates images via OpenAI DALL-E or videos via OpenRouter with polling; logic is sound with proper timeout and error handling. |
| packages/autogtm-core/src/socials/weeklyPlanner.ts | Pure allocation algorithm; correctly handles pinned themes, weighted random selection, and inventory tracking with no external dependencies. |
Comments Outside Diff (3)
-
apps/autogtm/src/inngest/functions.ts, line 476-477 (link)Guard never triggers —
limit: 1makes length ≤ 1 alwayslistSocialPostsis called withlimit: 1, soallItemscan contain at most one element. The subsequentif (allItems.length > 5000) returncan therefore never be true, meaning the circuit-breaker meant to prevent over-planning for companies with huge backlogs is completely dead code. Replace the query with one that returns a real count, or remove the guard entirely if it is no longer needed. -
migrations/20260427_socials.sql, line 133-185 (link)RLS policies allow any authenticated user to access every company's data
All seven policies use
auth.role() = 'authenticated'as the sole predicate, meaning any logged-in user can read and write every company's social themes, posts, schedules, and publish runs. There is no check tying rows to the requesting user's company. A proper policy should verify ownership, for example:create policy "Company members can manage social_posts" on social_posts for all using ( company_id in ( select company_id from company_members where user_id = auth.uid() ) );
Apply equivalent company-scoped predicates to all seven tables.
-
packages/autogtm-core/src/ai/draftSocialPost.ts, line 169 (link)Unguarded
JSON.parse— parse failure leaves post stuck inplannedstatusSame pattern as
classifySocialItems: if the model returns text that does not parse as JSON, the error propagates out of thegenerate-draft-copyInngest step. Withretries: 1the post is never updated topending_reviewand remains inplannedstatus indefinitely. Wrap the parse in a try/catch and re-throw with a descriptive message so monitoring can detect the failure.
Reviews (1): Last reviewed commit: "Add support for socials sectoin" | Re-trigger Greptile
| const items = await listSocialDataItems(companyId, { | ||
| status: 'classified', | ||
| themeId, | ||
| limit: 1, | ||
| }); | ||
| const item = items[0]; | ||
| if (!item) { | ||
| return NextResponse.json({ error: 'No ready ideas available for this theme' }, { status: 400 }); | ||
| } | ||
|
|
||
| const scheduledFor = body.scheduled_for || new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); | ||
| const posts = await createSocialPosts([ | ||
| { | ||
| company_id: companyId, | ||
| theme_id: themeId, | ||
| data_item_id: item.id, | ||
| week_plan_id: null, | ||
| slot_index: null, | ||
| scheduled_for: scheduledFor, | ||
| status: 'pending_review', | ||
| }, | ||
| ]); | ||
|
|
||
| const post = posts[0]; | ||
| if (!post) { | ||
| return NextResponse.json({ error: 'Failed to create post' }, { status: 500 }); | ||
| } | ||
|
|
||
| await inngest.send({ | ||
| name: 'autogtm/social.draft-post', | ||
| data: { companyId, postId: post.id }, | ||
| }); | ||
|
|
||
| return NextResponse.json({ |
There was a problem hiding this comment.
Item not reserved before async drafting — double-assignment race condition
The selected item stays in classified status until the Inngest draftSocialPost function later calls updateSocialDataItem(..., { status: 'used' }). Between creating the post and the async function running, a second call to this endpoint (or the weekly planner's cron) can pick up the same item, assigning it to two different posts simultaneously. The weekly planner explicitly sets items to reserved synchronously before handing off to Inngest — this route should do the same.
| raw_text: item.raw_text, | ||
| suggested_theme_id: item.suggested_theme_id || null, | ||
| structured: item.structured, | ||
| confidence: item.confidence, |
There was a problem hiding this comment.
No try/catch around JSON.parse — malformed LLM output throws and fails the whole dump
JSON.parse(jsonText) will throw a SyntaxError if the model returns non-JSON output (e.g. a refusal, rate-limit message, or partial response). The exception propagates out of classifySocialItems, crashes the processSocialDump Inngest step, and—because retries: 1—leaves the dump permanently stuck in processing status. Wrap the parse in a try/catch and either throw a clear error (so the dump is marked failed) or return an empty classification array.
| image_status text not null default 'not_generated' check (image_status in ('not_generated', 'generating', 'generated', 'failed')), | ||
| scheduled_for timestamptz not null, | ||
| status text not null default 'planned' check ( | ||
| status in ('planned', 'pending_review', 'approved', 'image_ready', 'published', 'failed', 'cancelled') | ||
| ), | ||
| postiz_post_id text, | ||
| postiz_release_id text, | ||
| error text, | ||
| published_at timestamptz, | ||
| created_at timestamptz not null default now(), | ||
| updated_at timestamptz not null default now() | ||
| ); | ||
| create index if not exists social_posts_company_schedule_idx on social_posts(company_id, scheduled_for); |
There was a problem hiding this comment.
used_for_post_id lacks a foreign-key constraint
social_data_items.used_for_post_id is declared as plain uuid without a reference to social_posts(id). If a post is deleted the column silently retains a dangling UUID, which can confuse queries that check this field to determine usage status. Add the FK:
used_for_post_id uuid references social_posts(id) on delete set null,
No description provided.