Files
Aldo Jimenez e4686cb9ee fix: seed script fails due to missing content field
The seed script fetches from /prompts.json without the ?full_content=true
parameter. The API only returns contentPreview by default, so the content
field is undefined, causing PrismaClientValidationError for every prompt.

Changes:
- Add ?full_content=true to the fetch URL
- Make content optional in RemotePrompt interface
- Skip prompts without content instead of failing

Before: Created 0 prompts (1595 skipped) with validation errors
After: Created 1576 prompts (19 skipped - no content)
2026-03-30 13:56:31 -04:00

324 lines
9.5 KiB
TypeScript

import { PrismaClient, PromptType, StructuredFormat, RequiredMediaType } from "@prisma/client";
import bcrypt from "bcryptjs";
const prisma = new PrismaClient();
const PROMPTS_JSON_URL = "https://prompts.chat/prompts.json?full_content=true";
/** Raw shape returned by the remote API — content may be absent. */
interface RemotePromptRaw {
id: string;
title: string;
slug: string;
description: string | null;
content?: string;
type: string;
structuredFormat: string | null;
mediaUrl: string | null;
viewCount: number;
voteCount: number;
commentCount: number;
isFeatured: boolean;
featuredAt: string | null;
requiresMediaUpload: boolean;
requiredMediaType: string | null;
requiredMediaCount: number | null;
createdAt: string;
updatedAt: string;
category: {
id: string;
name: string;
slug: string;
icon: string | null;
} | null;
author: {
username: string;
name: string | null;
avatar: string | null;
identifier: string;
verified: boolean;
};
contributors: Array<{
username: string;
name: string | null;
avatar: string | null;
identifier: string;
verified: boolean;
}>;
tags: Array<{
id: string;
name: string;
slug: string;
color: string;
}>;
}
/** Validated prompt with required content field. */
type RemotePrompt = RemotePromptRaw & { content: string };
/** Type guard that narrows a raw API prompt to a validated RemotePrompt. */
function hasContent(prompt: RemotePromptRaw): prompt is RemotePrompt {
return typeof prompt.content === "string" && prompt.content.length > 0;
}
interface RemotePromptsResponse {
count: number;
prompts: RemotePromptRaw[];
}
/** Fetches all prompts from the remote prompts.chat API with full content. */
async function fetchPrompts(): Promise<RemotePromptsResponse> {
console.log(`📡 Fetching prompts from ${PROMPTS_JSON_URL}...`);
const response = await fetch(PROMPTS_JSON_URL);
if (!response.ok) {
throw new Error(`Failed to fetch prompts: ${response.status} ${response.statusText}`);
}
const data = await response.json();
console.log(`✅ Fetched ${data.count} prompts`);
return data;
}
/** Seeds the database with users, categories, tags, and prompts from prompts.chat. */
async function main() {
console.log("🌱 Seeding database from prompts.chat...");
// Create admin user for assigning prompts
const password = await bcrypt.hash("password123", 12);
const admin = await prisma.user.upsert({
where: { email: "admin@prompts.chat" },
update: {},
create: {
email: "admin@prompts.chat",
username: "admin",
name: "Admin User",
password: password,
role: "ADMIN",
locale: "en",
},
});
console.log("✅ Created admin user");
// Fetch prompts from remote JSON
const { prompts: remotePrompts } = await fetchPrompts();
// Extract unique categories from remote prompts
const categoryMap = new Map<string, { name: string; slug: string; icon: string | null }>();
for (const prompt of remotePrompts) {
if (prompt.category) {
categoryMap.set(prompt.category.slug, {
name: prompt.category.name,
slug: prompt.category.slug,
icon: prompt.category.icon,
});
}
}
// Create categories
console.log(`📁 Creating ${categoryMap.size} categories...`);
const categoryIdMap = new Map<string, string>();
let categoryOrder = 1;
for (const [slug, cat] of categoryMap) {
const category = await prisma.category.upsert({
where: { slug },
update: { name: cat.name, icon: cat.icon },
create: {
name: cat.name,
slug: cat.slug,
icon: cat.icon,
order: categoryOrder++,
},
});
categoryIdMap.set(slug, category.id);
}
console.log(`✅ Created ${categoryMap.size} categories`);
// Extract unique tags from remote prompts
const tagMap = new Map<string, { name: string; slug: string; color: string }>();
for (const prompt of remotePrompts) {
for (const tag of prompt.tags) {
tagMap.set(tag.slug, {
name: tag.name,
slug: tag.slug,
color: tag.color,
});
}
}
// Create tags
console.log(`🏷️ Creating ${tagMap.size} tags...`);
const tagIdMap = new Map<string, string>();
for (const [slug, tag] of tagMap) {
const createdTag = await prisma.tag.upsert({
where: { slug },
update: { name: tag.name, color: tag.color },
create: {
name: tag.name,
slug: tag.slug,
color: tag.color,
},
});
tagIdMap.set(slug, createdTag.id);
}
console.log(`✅ Created ${tagMap.size} tags`);
// Extract unique authors from remote prompts and create users
const authorMap = new Map<string, { username: string; name: string | null; avatar: string | null; verified: boolean }>();
for (const prompt of remotePrompts) {
if (!authorMap.has(prompt.author.username)) {
authorMap.set(prompt.author.username, {
username: prompt.author.username,
name: prompt.author.name,
avatar: prompt.author.avatar,
verified: prompt.author.verified,
});
}
}
// Create users for authors
console.log(`👤 Creating ${authorMap.size} users...`);
const userIdMap = new Map<string, string>();
for (const [username, author] of authorMap) {
// Skip creating if it's the admin
if (username === "admin") {
userIdMap.set(username, admin.id);
continue;
}
const user = await prisma.user.upsert({
where: { username },
update: { name: author.name, avatar: author.avatar },
create: {
email: `${username}@prompts.chat`,
username: author.username,
name: author.name,
avatar: author.avatar,
password: password,
role: "USER",
locale: "en",
},
});
userIdMap.set(username, user.id);
}
console.log(`✅ Created ${authorMap.size} users`);
// Create prompts
console.log(`📝 Creating ${remotePrompts.length} prompts...`);
let promptsCreated = 0;
let promptsSkipped = 0;
for (const remotePrompt of remotePrompts) {
const authorId = userIdMap.get(remotePrompt.author.username);
if (!authorId) {
console.warn(`⚠️ Skipping prompt "${remotePrompt.title}" - author not found`);
promptsSkipped++;
continue;
}
const categoryId = remotePrompt.category ? categoryIdMap.get(remotePrompt.category.slug) : null;
// Map prompt type
const typeMap: Record<string, PromptType> = {
TEXT: "TEXT",
IMAGE: "IMAGE",
VIDEO: "VIDEO",
AUDIO: "AUDIO",
STRUCTURED: "STRUCTURED",
SKILL: "SKILL",
};
const promptType = typeMap[remotePrompt.type] || "TEXT";
// Map structured format
const formatMap: Record<string, StructuredFormat> = {
JSON: "JSON",
YAML: "YAML",
};
const structuredFormat = remotePrompt.structuredFormat ? formatMap[remotePrompt.structuredFormat] : null;
// Skip prompts without content
if (!hasContent(remotePrompt)) {
console.warn(`⚠ Skipping prompt "${remotePrompt.title}" - no content`);
promptsSkipped++;
continue;
}
// Check if prompt already exists
const existingPrompt = await prisma.prompt.findFirst({
where: { slug: remotePrompt.slug },
});
if (existingPrompt) {
promptsSkipped++;
continue;
}
try {
const prompt = await prisma.prompt.create({
data: {
title: remotePrompt.title,
slug: remotePrompt.slug,
description: remotePrompt.description,
content: remotePrompt.content,
type: promptType,
structuredFormat: structuredFormat,
mediaUrl: remotePrompt.mediaUrl,
viewCount: remotePrompt.viewCount,
isFeatured: remotePrompt.isFeatured,
featuredAt: remotePrompt.featuredAt ? new Date(remotePrompt.featuredAt) : null,
requiresMediaUpload: remotePrompt.requiresMediaUpload,
requiredMediaType: remotePrompt.requiredMediaType as RequiredMediaType | null,
requiredMediaCount: remotePrompt.requiredMediaCount,
authorId: authorId,
categoryId: categoryId,
tags: {
create: remotePrompt.tags
.filter(tag => tagIdMap.has(tag.slug))
.map(tag => ({ tagId: tagIdMap.get(tag.slug)! })),
},
},
});
// Create initial version
await prisma.promptVersion.create({
data: {
promptId: prompt.id,
version: 1,
content: remotePrompt.content,
changeNote: "Initial version",
createdBy: authorId,
},
});
promptsCreated++;
} catch (error) {
console.warn(`⚠️ Failed to create prompt "${remotePrompt.title}":`, error);
promptsSkipped++;
}
}
console.log(`✅ Created ${promptsCreated} prompts (${promptsSkipped} skipped)`);
// Fail-fast if no prompts were created — likely an upstream API regression
const total = promptsCreated + promptsSkipped;
if (total > 0 && promptsCreated === 0) {
throw new Error(
`Seeding failed: 0 prompts created out of ${total} (${promptsSkipped} skipped). ` +
`This likely indicates an upstream API regression — check that ${PROMPTS_JSON_URL} returns content.`
);
}
console.log("\n🎉 Seeding complete!");
console.log("\n📋 Test credentials (password: password123):");
console.log(" Admin: admin@prompts.chat");
}
main()
.catch((e) => {
console.error("❌ Seeding failed:", e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});