From b277ceb9a0a03edf7d720ad09c63683626190444 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 1 Oct 2025 18:23:02 +0200 Subject: [PATCH] feat(web): add database seeding script for testing Add seed-db.ts script to populate PocketBase database with fake submission data. Creates test users, downloads icon images from CDN, and generates random submissions with various statuses (pending, approved, rejected, added_to_collection) for development and testing purposes --- web/seed-db.ts | 216 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 web/seed-db.ts diff --git a/web/seed-db.ts b/web/seed-db.ts new file mode 100644 index 00000000..06e967c6 --- /dev/null +++ b/web/seed-db.ts @@ -0,0 +1,216 @@ +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import { pb, type User } from './src/lib/pb.js' + +// Constants (matching src/constants.ts) +const BASE_URL = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons" + +interface IconMetadata { + base: string + aliases: string[] + categories: string[] + update: { + timestamp: string + author: { + id: number + login?: string + name?: string + } + } +} + +interface MetadataFile { + [key: string]: IconMetadata +} + +const STATUSES = ['pending', 'approved', 'rejected', 'added_to_collection'] as const + +async function getOrCreateUser(email: string, username: string, password: string = 'password123'): Promise { + try { + // Try to authenticate first (if user exists, this will work) + try { + await pb.collection('users').authWithPassword(email, password, { + requestKey: null + }) + const user = pb.authStore.record as unknown as User + console.log(`āœ“ Authenticated existing user: ${email}`) + return user + } catch (authError) { + // User doesn't exist or wrong password, try to create + console.log(` User ${email} not found, creating...`) + } + + // Create new user if doesn't exist + const user = await pb.collection('users').create({ + email, + username, + password, + passwordConfirm: password, + emailVisibility: true + }, { + requestKey: null + }) + + console.log(`āœ“ Created new user: ${email}`) + + // Authenticate with the newly created user + await pb.collection('users').authWithPassword(email, password, { + requestKey: null + }) + + return user + } catch (error: any) { + console.error(`āœ— Error with user ${email}:`, error?.message || error) + throw error + } +} + +async function downloadImage(iconName: string, format: 'svg' | 'png' | 'webp'): Promise { + const url = `${BASE_URL}/${format}/${iconName}.${format}` + console.log(` Downloading: ${url}`) + + const response = await fetch(url) + if (!response.ok) { + throw new Error(`Failed to download ${url}: ${response.statusText}`) + } + + // Get the blob from the response + const blob = await response.blob() + + // Create a File instance from the blob (like in PocketBase docs) + const file = new File([blob], `${iconName}.${format}`, { + type: blob.type || `image/${format === 'svg' ? 'svg+xml' : format}` + }) + + return file +} + +async function createFakeSubmission( + iconName: string, + iconData: IconMetadata, + user: User, + approvedById?: string +) { + try { + console.log(`\nšŸ“ Creating submission for: ${iconName} (as ${user.email})`) + + // Authenticate as the user who will create the submission + console.log(` šŸ” Authenticating as ${user.email}...`) + await pb.collection('users').authWithPassword(user.email, 'password123', { + requestKey: null + }) + + // Download the image based on the base format (returns File instance) + const format = iconData.base as 'svg' | 'png' | 'webp' + const file = await downloadImage(iconName, format) + + // Randomly select a status + const status = STATUSES[Math.floor(Math.random() * STATUSES.length)] + + // Prepare submission data (like in PocketBase docs) + const submissionData: Record = { + name: iconName, + assets: [file], // files must be Blob or File instances + created_by: user.id, + status: status, + extras: { + aliases: iconData.aliases || [], + categories: iconData.categories || [], + base: iconData.base + } + } + + // Only add approved_by if status is approved or added_to_collection + if ((status === 'approved' || status === 'added_to_collection') && approvedById) { + submissionData.approved_by = approvedById + } + + const submission = await pb.collection('submissions').create(submissionData, { + requestKey: null // Disable auto-cancellation + }) + + console.log(`āœ“ Created submission: ${iconName} (${status})`) + return submission + } catch (error: any) { + console.error(`āœ— Failed to create submission for ${iconName}:`, error?.message || error) + if (error?.response) { + console.error(' Response:', JSON.stringify(error.response, null, 2)) + } + if (error?.data) { + console.error(' Data:', JSON.stringify(error.data, null, 2)) + } + throw error + } +} + +async function main() { + console.log('šŸš€ Starting fake submissions generator\n') + + // Read metadata.json + const metadataPath = join(process.cwd(), '..', 'metadata.json') + console.log(`šŸ“– Reading metadata from: ${metadataPath}`) + + const metadataContent = await readFile(metadataPath, 'utf-8') + const metadata: MetadataFile = JSON.parse(metadataContent) + + const iconNames = Object.keys(metadata) + console.log(`āœ“ Found ${iconNames.length} icons in metadata\n`) + + // Create or get users sequentially to avoid conflicts + console.log('šŸ‘„ Setting up users...') + const user1 = await getOrCreateUser('user1@example.com', 'user1') + const user2 = await getOrCreateUser('user2@example.com', 'user2') + const user3 = await getOrCreateUser('user3@example.com', 'user3') + const adminUser = await getOrCreateUser('admin@example.com', 'admin') + + const users = [user1, user2, user3, adminUser] + + // Select random number of icons to create submissions for + const numberOfSubmissions = parseInt(process.argv[2]) || 5 + console.log(`\nšŸŽ² Creating ${numberOfSubmissions} random submissions...\n`) + + const selectedIndices = new Set() + while (selectedIndices.size < numberOfSubmissions) { + selectedIndices.add(Math.floor(Math.random() * iconNames.length)) + } + + const submissions = [] + for (const index of selectedIndices) { + const iconName = iconNames[index] + const iconData = metadata[iconName] + + // Randomly select a user + const randomUser = users[Math.floor(Math.random() * users.length)] + + try { + const submission = await createFakeSubmission( + iconName, + iconData, + randomUser, // Pass full user object + adminUser.id + ) + submissions.push(submission) + } catch (error: any) { + console.error(`āœ— Skipping ${iconName} due to error:`, error?.message || error) + if (error?.data) { + console.error(' Error details:', JSON.stringify(error.data, null, 2)) + } + } + } + + console.log(`\n✨ Successfully created ${submissions.length} submissions!`) + console.log('\nšŸ“Š Summary:') + console.log(` - Pending: ${submissions.filter(s => s.status === 'pending').length}`) + console.log(` - Approved: ${submissions.filter(s => s.status === 'approved').length}`) + console.log(` - Rejected: ${submissions.filter(s => s.status === 'rejected').length}`) + console.log(` - Added to Collection: ${submissions.filter(s => s.status === 'added_to_collection').length}`) + + // Clear auth store + pb.authStore.clear() +} + +main().catch((error) => { + console.error('\nāŒ Fatal error:', error) + process.exit(1) +}) +