Add Supabase demo feed seeding endpoint
cgen-83da51189f5f451aadffb55afb7bcfc2
This commit is contained in:
parent
f0427f74ec
commit
3c499df528
1 changed files with 298 additions and 0 deletions
298
api/community/seed-demo.ts
Normal file
298
api/community/seed-demo.ts
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
import type { VercelRequest, VercelResponse } from "@vercel/node";
|
||||
import { getAdminClient } from "../_supabase";
|
||||
|
||||
interface DemoUser {
|
||||
email: string;
|
||||
fullName: string;
|
||||
username: string;
|
||||
avatarUrl: string;
|
||||
bio: string;
|
||||
location: string;
|
||||
experienceLevel: "beginner" | "intermediate" | "advanced" | "expert";
|
||||
}
|
||||
|
||||
interface DemoPost {
|
||||
id: string;
|
||||
authorEmail: string;
|
||||
title: string;
|
||||
content: {
|
||||
text: string;
|
||||
mediaUrl: string | null;
|
||||
mediaType: "video" | "image" | "none";
|
||||
};
|
||||
category: string;
|
||||
tags: string[];
|
||||
likes: number;
|
||||
comments: number;
|
||||
hoursAgo: number;
|
||||
}
|
||||
|
||||
const DEMO_USERS: DemoUser[] = [
|
||||
{
|
||||
email: "updates@aethex.dev",
|
||||
fullName: "AeThex Updates",
|
||||
username: "aethex",
|
||||
avatarUrl:
|
||||
"https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F3979ec9a8a28471d900a80e94e2c45fe?format=png&width=256",
|
||||
bio: "Official AeThex OS updates, roadmap signals, and community spotlights.",
|
||||
location: "AeThex HQ",
|
||||
experienceLevel: "expert",
|
||||
},
|
||||
{
|
||||
email: "labs@aethex.dev",
|
||||
fullName: "AeThex Labs",
|
||||
username: "aethexlabs",
|
||||
avatarUrl: "https://i.pravatar.cc/150?img=8",
|
||||
bio: "Experimental builds, prototypes, and R&D drops from the Labs team.",
|
||||
location: "Global",
|
||||
experienceLevel: "advanced",
|
||||
},
|
||||
{
|
||||
email: "mrpiglr+demo@aethex.dev",
|
||||
fullName: "Mr Piglr",
|
||||
username: "mrpiglr",
|
||||
avatarUrl: "https://i.pravatar.cc/150?img=11",
|
||||
bio: "Testing the admin pipeline and validating AeThex OS features.",
|
||||
location: "AeThex Command Center",
|
||||
experienceLevel: "expert",
|
||||
},
|
||||
];
|
||||
|
||||
const DEMO_POSTS: DemoPost[] = [
|
||||
{
|
||||
id: "f4dd3f65-462c-4b54-8d75-1830d5c6a001",
|
||||
authorEmail: "labs@aethex.dev",
|
||||
title: "Lab Drop: Procedural City Showcase",
|
||||
content: {
|
||||
text: "Fresh from the render farm — a procedural city loop rendered directly in AeThex Forge. Toggle sound for spatial audio cues!",
|
||||
mediaUrl: "https://storage.googleapis.com/coverr-main/mp4/Mt_Baker.mp4",
|
||||
mediaType: "video",
|
||||
},
|
||||
category: "video",
|
||||
tags: ["labs", "procedural", "video"],
|
||||
likes: 128,
|
||||
comments: 26,
|
||||
hoursAgo: 3,
|
||||
},
|
||||
{
|
||||
id: "f4dd3f65-462c-4b54-8d75-1830d5c6a002",
|
||||
authorEmail: "updates@aethex.dev",
|
||||
title: "AeThex OS 0.9.4 release thread",
|
||||
content: {
|
||||
text: "Release 0.9.4 is live with the refreshed dashboard, passport syncing, and the new admin panel. Patch notes compiled in the docs portal!",
|
||||
mediaUrl:
|
||||
"https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F74274d2890c845a2ade125b075444ef2?format=webp&width=1600",
|
||||
mediaType: "image",
|
||||
},
|
||||
category: "image",
|
||||
tags: ["release", "aethex", "dashboard"],
|
||||
likes: 94,
|
||||
comments: 14,
|
||||
hoursAgo: 6,
|
||||
},
|
||||
{
|
||||
id: "f4dd3f65-462c-4b54-8d75-1830d5c6a003",
|
||||
authorEmail: "mrpiglr+demo@aethex.dev",
|
||||
title: "Admin panel QA checklist",
|
||||
content: {
|
||||
text: "Running through the QA list for the admin suite. Permissions, member modals, and achievement tooling all check out. Logs look clean!",
|
||||
mediaUrl: null,
|
||||
mediaType: "none",
|
||||
},
|
||||
category: "text",
|
||||
tags: ["qa", "admin", "update"],
|
||||
likes: 37,
|
||||
comments: 5,
|
||||
hoursAgo: 9,
|
||||
},
|
||||
{
|
||||
id: "f4dd3f65-462c-4b54-8d75-1830d5c6a004",
|
||||
authorEmail: "updates@aethex.dev",
|
||||
title: "Community shout-out",
|
||||
content: {
|
||||
text: "Huge shout-out to the AeThex community members posting their prototypes today. The energy in the feed is 🔥",
|
||||
mediaUrl:
|
||||
"https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Ffef86bb69cf147a1a8614048d2d70502?format=webp&width=1600",
|
||||
mediaType: "image",
|
||||
},
|
||||
category: "image",
|
||||
tags: ["community", "highlight"],
|
||||
likes: 76,
|
||||
comments: 9,
|
||||
hoursAgo: 12,
|
||||
},
|
||||
];
|
||||
|
||||
const FOLLOW_PAIRS: Array<{ followerEmail: string; followingEmail: string }> = [
|
||||
{ followerEmail: "mrpiglr+demo@aethex.dev", followingEmail: "updates@aethex.dev" },
|
||||
{ followerEmail: "mrpiglr+demo@aethex.dev", followingEmail: "labs@aethex.dev" },
|
||||
{ followerEmail: "labs@aethex.dev", followingEmail: "updates@aethex.dev" },
|
||||
];
|
||||
|
||||
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const admin = getAdminClient();
|
||||
const userMap = new Map<string, string>();
|
||||
const seededUsers: any[] = [];
|
||||
|
||||
for (const demoUser of DEMO_USERS) {
|
||||
const { data: searchResult, error: searchError } = await admin.auth.admin
|
||||
.listUsers({ email: demoUser.email });
|
||||
if (searchError) throw searchError;
|
||||
|
||||
let authUser = searchResult.users?.[0];
|
||||
if (!authUser) {
|
||||
const tempPassword = `Demo${Math.random().toString(36).slice(2, 10)}!9`;
|
||||
const { data: createdUser, error: createError } =
|
||||
await admin.auth.admin.createUser({
|
||||
email: demoUser.email,
|
||||
password: tempPassword,
|
||||
email_confirm: true,
|
||||
user_metadata: { full_name: demoUser.fullName },
|
||||
});
|
||||
if (createError) throw createError;
|
||||
authUser = createdUser.user ?? undefined;
|
||||
}
|
||||
|
||||
if (!authUser) continue;
|
||||
|
||||
userMap.set(demoUser.email, authUser.id);
|
||||
|
||||
const { data: existingProfile, error: profileLookupError } = await admin
|
||||
.from("user_profiles")
|
||||
.select("*")
|
||||
.eq("id", authUser.id)
|
||||
.maybeSingle();
|
||||
if (profileLookupError && profileLookupError.code !== "PGRST116") {
|
||||
throw profileLookupError;
|
||||
}
|
||||
|
||||
if (!existingProfile) {
|
||||
const profilePayload = {
|
||||
id: authUser.id,
|
||||
full_name: demoUser.fullName,
|
||||
username: demoUser.username,
|
||||
avatar_url: demoUser.avatarUrl,
|
||||
bio: demoUser.bio,
|
||||
location: demoUser.location,
|
||||
user_type: "community_member" as const,
|
||||
experience_level: demoUser.experienceLevel,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
const { data: insertedProfile, error: insertProfileError } = await admin
|
||||
.from("user_profiles")
|
||||
.insert(profilePayload)
|
||||
.select()
|
||||
.single();
|
||||
if (insertProfileError) throw insertProfileError;
|
||||
seededUsers.push(insertProfileError ? null : insertedProfile);
|
||||
} else {
|
||||
seededUsers.push(existingProfile);
|
||||
}
|
||||
}
|
||||
|
||||
const seededPosts: any[] = [];
|
||||
const now = Date.now();
|
||||
|
||||
for (const post of DEMO_POSTS) {
|
||||
const authorId = userMap.get(post.authorEmail);
|
||||
if (!authorId) continue;
|
||||
|
||||
const { data: existingPost, error: postLookupError } = await admin
|
||||
.from("community_posts")
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
user_profiles (
|
||||
username,
|
||||
full_name,
|
||||
avatar_url
|
||||
)
|
||||
`,
|
||||
)
|
||||
.eq("id", post.id)
|
||||
.maybeSingle();
|
||||
if (postLookupError && postLookupError.code !== "PGRST116") {
|
||||
throw postLookupError;
|
||||
}
|
||||
|
||||
if (existingPost) {
|
||||
seededPosts.push(existingPost);
|
||||
continue;
|
||||
}
|
||||
|
||||
const createdAt = new Date(now - post.hoursAgo * 3600 * 1000).toISOString();
|
||||
const { data: insertedPost, error: insertPostError } = await admin
|
||||
.from("community_posts")
|
||||
.insert({
|
||||
id: post.id,
|
||||
author_id: authorId,
|
||||
title: post.title,
|
||||
content: JSON.stringify(post.content),
|
||||
category: post.category,
|
||||
tags: post.tags,
|
||||
likes_count: post.likes,
|
||||
comments_count: post.comments,
|
||||
is_published: true,
|
||||
created_at: createdAt,
|
||||
updated_at: createdAt,
|
||||
})
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
user_profiles (
|
||||
username,
|
||||
full_name,
|
||||
avatar_url
|
||||
)
|
||||
`,
|
||||
)
|
||||
.single();
|
||||
if (insertPostError) throw insertPostError;
|
||||
seededPosts.push(insertPostError ? null : insertedPost);
|
||||
}
|
||||
|
||||
const followRows = FOLLOW_PAIRS.flatMap(({ followerEmail, followingEmail }) => {
|
||||
const followerId = userMap.get(followerEmail);
|
||||
const followingId = userMap.get(followingEmail);
|
||||
if (!followerId || !followingId) return [];
|
||||
return [
|
||||
{
|
||||
follower_id: followerId,
|
||||
following_id: followingId,
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
if (followRows.length) {
|
||||
const { error: followError } = await admin
|
||||
.from("user_follows")
|
||||
.upsert(followRows, {
|
||||
onConflict: "follower_id,following_id" as any,
|
||||
ignoreDuplicates: true as any,
|
||||
});
|
||||
if (followError) throw followError;
|
||||
}
|
||||
|
||||
const sanitizedUsers = seededUsers.filter(Boolean);
|
||||
const sanitizedPosts = seededPosts.filter(Boolean);
|
||||
|
||||
return res.status(200).json({
|
||||
ok: true,
|
||||
usersSeeded: sanitizedUsers.length,
|
||||
postsSeeded: sanitizedPosts.length,
|
||||
posts: sanitizedPosts,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Demo feed seeding failed", error);
|
||||
return res.status(500).json({
|
||||
error: error?.message || "Unable to seed demo feed",
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue