//! Seed initial forum data for development. Run with `--seed` flag. //! //! The orchestration lives here; the rows it writes are in `rows`, the people //! who write them in `users`, and the forum content itself in `content`, which //! is data rather than code and was most of this file's size. mod content; mod rows; mod users; use content::{seed_music_general, seed_music_mixing, seed_music_sound_design}; use rows::{ seed_category, seed_community, seed_membership, seed_membership_upsert, seed_post, seed_thread, }; use users::{seed_harness_users, seed_users}; use mt_core::types::CommunityRole; use sqlx::PgPool; pub async fn run(pool: &PgPool) { // Guard: skip if data already exists (threads have no unique constraint) let thread_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM threads") .fetch_one(pool) .await .unwrap_or(0); if thread_count > 0 { tracing::info!("seed data already exists, skipping"); return; } // --- users let users = seed_users(pool).await; let harness = seed_harness_users(pool).await; // --- communities let rust_id = seed_community( pool, "Rust Programming", "rust", Some("All things Rust: language, ecosystem, tooling."), ) .await; let music_id = seed_community( pool, "Music Production", "music", Some("DAWs, plugins, mixing, mastering, sound design."), ) .await; let selfhosted_id = seed_community( pool, "Self-Hosted", "selfhosted", Some("Running your own infrastructure. No cloud required."), ) .await; // --- categories // Rust let rust_general = seed_category( pool, rust_id, "General", "general", 0, Some("Anything that doesn't fit elsewhere."), ) .await; let rust_help = seed_category( pool, rust_id, "Help & Questions", "help", 1, Some("Ask for help, get answers."), ) .await; let _rust_show = seed_category( pool, rust_id, "Show & Tell", "show", 2, Some("Share what you've built."), ) .await; let _rust_meta = seed_category( pool, rust_id, "Meta", "meta", 3, Some("Discussion about this community itself."), ) .await; // Music, the main test community let music_general = seed_category( pool, music_id, "General", "general", 0, Some("General music production discussion."), ) .await; let music_mixing = seed_category( pool, music_id, "Mixing & Mastering", "mixing", 1, Some("Techniques for getting a polished mix."), ) .await; let music_sound = seed_category( pool, music_id, "Sound Design", "sound-design", 2, Some("Synthesis, sampling, and sound creation."), ) .await; // Self-Hosted let sh_general = seed_category( pool, selfhosted_id, "General", "general", 0, Some("General self-hosting discussion."), ) .await; let _sh_homelab = seed_category( pool, selfhosted_id, "Homelab", "homelab", 1, Some("Hardware, networking, and home server setups."), ) .await; // --- memberships for user in &users { seed_membership(pool, user.id, music_id, CommunityRole::Member).await; } // admin owns all, maxmj is moderator of music seed_membership_upsert(pool, users[0].id, rust_id, CommunityRole::Owner).await; seed_membership_upsert(pool, users[0].id, music_id, CommunityRole::Owner).await; seed_membership_upsert(pool, users[0].id, selfhosted_id, CommunityRole::Owner).await; seed_membership_upsert(pool, users[1].id, rust_id, CommunityRole::Member).await; seed_membership_upsert(pool, users[1].id, music_id, CommunityRole::Moderator).await; // The harness accounts get the roles the browser axis needs to write: // ordinary membership for thread/reply/flag, and Owner + Moderator on two // different communities so a moderation action has somewhere to land. The // roles are seeded rather than granted at login because login only ever // upserts identity and MNW perks, never membership. for account in &harness { seed_membership_upsert(pool, account.id, rust_id, account.rust_role).await; seed_membership_upsert(pool, account.id, music_id, account.music_role).await; seed_membership_upsert(pool, account.id, selfhosted_id, CommunityRole::Member).await; } // --- Rust community: a few threads let welcome_id = seed_thread( pool, rust_general, users[0].id, "Welcome, read before posting", true, false, ) .await; seed_post(pool, welcome_id, users[0].id, "Welcome to the Rust Programming community. Please be respectful, stay on topic, and use code blocks for code snippets.").await; let async_id = seed_thread( pool, rust_general, users[1].id, "How do I get started with async Rust?", false, false, ) .await; seed_post(pool, async_id, users[1].id, "I've been writing synchronous Rust for a few months and want to start using async/await. What runtime should I pick? Is tokio the only option?\n\nAny recommended tutorials or blog posts would be great.").await; seed_post(pool, async_id, users[0].id, "Tokio is the most popular and what most web frameworks (Axum, Actix) use. There's also `async-std` and `smol`, but the ecosystem gravitates toward tokio.\n\nStart with the tokio tutorial: it covers spawning tasks, channels, and I/O.").await; let error_id = seed_thread( pool, rust_help, users[1].id, "Best practices for error handling in Axum", false, false, ) .await; seed_post(pool, error_id, users[1].id, "What's the recommended way to handle errors in Axum handlers? Should I use `anyhow`, `thiserror`, or something else? I keep writing `.map_err(|e| ...)` everywhere.").await; // --- Self-Hosted: a few threads let caddy_id = seed_thread( pool, sh_general, users[0].id, "Caddy vs nginx for reverse proxy", false, false, ) .await; seed_post(pool, caddy_id, users[0].id, "I have been using nginx for years but Caddy's automatic HTTPS is tempting. Anyone made the switch? What are the tradeoffs?").await; seed_post(pool, caddy_id, users[3].id, "Switched last year. Caddy's config is so much simpler. Automatic cert renewal is great. Only downside is slightly higher memory usage but it is negligible for small setups.").await; // --- Music community: bulk seed seed_music_general(pool, music_general, &users).await; seed_music_mixing(pool, music_mixing, &users).await; seed_music_sound_design(pool, music_sound, &users).await; // reply_count is computed live at read time, nothing to backfill. let total_threads: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM threads") .fetch_one(pool) .await .unwrap_or(0); let total_posts: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM posts") .fetch_one(pool) .await .unwrap_or(0); tracing::info!( "seeded 3 communities, {} users, {} threads, {} posts", users.len(), total_threads, total_posts ); }