···11+# Database Scripts
22+33+## Seed Test Questions
44+55+The `seed-test-questions.ts` script creates test data for local development:
66+77+### Usage:
88+99+#### Option 1: Use with real user handles (recommended)
1010+```bash
1111+# First, login as real users through the web app, then:
1212+pnpm run db:seed your-handle.bsky.social another-user.bsky.social
1313+1414+# Example:
1515+pnpm run db:seed alice.bsky.social bob.bsky.social charlie.bsky.social
1616+```
1717+1818+#### Option 2: Use default test users (for testing only)
1919+```bash
2020+# Creates fake test users that you can't actually login as
2121+pnpm run db:seed
2222+```
2323+2424+### What it creates:
2525+- **Questions between real users** (if handles provided) or **3 fake test users** (fallback)
2626+- **10 diverse programming-related questions** distributed between users
2727+- **Mix of regular and anonymous questions**
2828+2929+### Features:
3030+- ✅ **Works with real user accounts** - provide handles of users who have logged in
3131+- ✅ **Safe fallback** - creates fake test users if no handles provided
3232+- ✅ **Smart user detection** - checks if provided handles exist in database
3333+- ✅ **Round-robin question distribution** - spreads questions evenly between users
3434+- ✅ **Safe to run multiple times** (uses `onConflictDoNothing()` for users)
3535+- ✅ **Clear progress logging** with emojis and status updates
3636+- ✅ **Provides summary statistics** at the end
3737+3838+### Example Questions Include:
3939+- "What's your favorite programming language and why?"
4040+- "How do you stay motivated when working on long-term projects?"
4141+- "What's the most interesting bug you've ever encountered?"
4242+- "If you could give advice to your past self when you started coding, what would it be?"
4343+- "What's your go-to method for debugging complex issues?"
4444+- And more...
4545+4646+### Requirements:
4747+- **For real users**: Users must have logged in through the web app at least once
4848+- **Minimum 2 users** needed to create questions between them
4949+5050+### Note:
5151+These are local test questions only - they are **not synced to the AT Protocol network**.
+73
scripts/cleanup-stale-users.ts
···11+#!/usr/bin/env tsx
22+33+/**
44+ * Removes stale user rows left behind after a PDS re-seed.
55+ *
66+ * --dry-run (default): shows what would be deleted
77+ * --run: actually deletes
88+ *
99+ * Usage:
1010+ * pnpm tsx scripts/cleanup-stale-users.ts # dry-run
1111+ * pnpm tsx scripts/cleanup-stale-users.ts --run # execute
1212+ */
1313+1414+import { sql, eq } from "drizzle-orm";
1515+import { db } from "../src/lib/db";
1616+import { users, questions, answers, sessions } from "../src/lib/schema";
1717+1818+const dryRun = !process.argv.includes("--run");
1919+2020+async function main() {
2121+ // Stale users = handle equals their DID (set by the callback conflict handler)
2222+ const stale = await db
2323+ .select({ did: users.did, handle: users.handle })
2424+ .from(users)
2525+ .where(sql`${users.handle} = ${users.did}`);
2626+2727+ if (stale.length === 0) {
2828+ console.log("No stale users (handle == DID) found. DB looks clean.");
2929+ process.exit(0);
3030+ }
3131+3232+ console.log(`Found ${stale.length} stale user(s):`);
3333+ for (const u of stale) console.log(` ${u.did}`);
3434+3535+ if (dryRun) {
3636+ console.log("\nDry-run — pass --run to actually delete.");
3737+ process.exit(0);
3838+ }
3939+4040+ for (const u of stale) {
4141+ await db.transaction(async (tx) => {
4242+ const questionRows = await tx
4343+ .select({ id: questions.id })
4444+ .from(questions)
4545+ .where(
4646+ sql`${questions.authorDid} = ${u.did} OR ${questions.targetDid} = ${u.did}`
4747+ );
4848+ const qIds = questionRows.map((r) => r.id);
4949+5050+ if (qIds.length > 0) {
5151+ await tx.delete(answers).where(sql`${answers.questionId} IN ${qIds}`);
5252+ }
5353+ await tx.delete(answers).where(eq(answers.authorDid, u.did));
5454+ await tx
5555+ .delete(questions)
5656+ .where(
5757+ sql`${questions.authorDid} = ${u.did} OR ${questions.targetDid} = ${u.did}`
5858+ );
5959+ await tx.delete(sessions).where(eq(sessions.did, u.did));
6060+ await tx.delete(users).where(eq(users.did, u.did));
6161+6262+ console.log(` Deleted user ${u.did} and related data.`);
6363+ });
6464+ }
6565+6666+ console.log("Done.");
6767+ process.exit(0);
6868+}
6969+7070+main().catch((err) => {
7171+ console.error("Cleanup failed:", err);
7272+ process.exit(1);
7373+});
+30
scripts/db-check.ts
···11+#!/usr/bin/env tsx
22+33+import { db } from "../src/lib/db";
44+import { users, questions, answers } from "../src/lib/schema";
55+66+async function main() {
77+ const u = await db.select().from(users);
88+ console.log("=== USERS ===", u.length, "total");
99+ for (const r of u)
1010+ console.log(
1111+ ` did=${r.did} handle=${r.handle} name=${r.displayName}`
1212+ );
1313+1414+ const q = await db.select().from(questions);
1515+ console.log("\n=== QUESTIONS ===", q.length, "total");
1616+ for (const r of q)
1717+ console.log(
1818+ ` id=${r.id.slice(0, 8)} from=${r.authorDid} to=${r.targetDid} "${r.content.slice(0, 50)}"`
1919+ );
2020+2121+ const a = await db.select().from(answers);
2222+ console.log("\n=== ANSWERS ===", a.length, "total");
2323+2424+ process.exit(0);
2525+}
2626+2727+main().catch((err) => {
2828+ console.error("DB check failed:", err);
2929+ process.exit(1);
3030+});
+81
scripts/dev-network.ts
···11+#!/usr/bin/env tsx
22+33+/**
44+ * Boots a local AT Protocol network (PDS + PLC) using @atproto/dev-env
55+ * and creates test accounts for alice and bob.
66+ *
77+ * The PDS includes a built-in OAuth Authorization Server, so the full
88+ * browser-based OAuth flow works against it.
99+ *
1010+ * Usage:
1111+ * pnpm run dev:network
1212+ *
1313+ * Then start the app with the printed env vars, or copy them into .env
1414+ */
1515+1616+import { TestNetworkNoAppView } from "@atproto/dev-env";
1717+1818+const ACCOUNTS = [
1919+ {
2020+ shortName: "alice",
2121+ handle: "alice.test",
2222+ email: "alice@test.com",
2323+ password: "alice-pass-123",
2424+ displayName: "Alice Test",
2525+ },
2626+ {
2727+ shortName: "bob",
2828+ handle: "bob.test",
2929+ email: "bob@test.com",
3030+ password: "bob-pass-123",
3131+ displayName: "Bob Test",
3232+ },
3333+];
3434+3535+async function main() {
3636+ console.log("Starting local AT Protocol network...\n");
3737+3838+ const network = await TestNetworkNoAppView.create({});
3939+4040+ const pdsUrl = network.pds.url;
4141+ const plcUrl = network.plc.url;
4242+4343+ console.log(`PDS running at: ${pdsUrl}`);
4444+ console.log(`PLC running at: ${plcUrl}\n`);
4545+4646+ const sc = network.getSeedClient();
4747+4848+ for (const account of ACCOUNTS) {
4949+ const { shortName, handle, email, password, displayName } = account;
5050+ await sc.createAccount(shortName, { handle, email, password });
5151+ const did = sc.dids[shortName];
5252+ await sc.createProfile(did, displayName, `Hi, I'm ${displayName}`);
5353+ console.log(`Account created: ${handle} (DID: ${did})`);
5454+ }
5555+5656+ await network.processAll();
5757+5858+ console.log("\n--- Environment variables for .env ---\n");
5959+ console.log(`DEV_PDS_URL=${pdsUrl}`);
6060+ console.log(`DEV_PLC_URL=${plcUrl}`);
6161+ console.log("");
6262+6363+ console.log("--- Test accounts ---\n");
6464+ for (const account of ACCOUNTS) {
6565+ const did = sc.dids[account.shortName];
6666+ console.log(` ${account.handle}`);
6767+ console.log(` DID: ${did}`);
6868+ console.log(` Email: ${account.email}`);
6969+ console.log(` Password: ${account.password}`);
7070+ console.log("");
7171+ }
7272+7373+ console.log("Network is running. Press Ctrl+C to stop.\n");
7474+7575+ await new Promise(() => {});
7676+}
7777+7878+main().catch((err) => {
7979+ console.error("Fatal error:", err);
8080+ process.exit(1);
8181+});
+237
scripts/seed-test-questions.ts
···11+#!/usr/bin/env tsx
22+33+import { db } from "../src/lib/db";
44+import { users, questions } from "../src/lib/schema";
55+66+// Parse command line arguments
77+const args = process.argv.slice(2);
88+99+// Show help if requested
1010+if (args.includes('--help') || args.includes('-h')) {
1111+ console.log(`
1212+🌱 Seed Test Questions Script
1313+1414+Usage:
1515+ pnpm run db:seed [handle1] [handle2] [handle3] ...
1616+ pnpm run db:seed --help
1717+1818+Examples:
1919+ # Use real user handles (recommended - users must have logged in first)
2020+ pnpm run db:seed alice.bsky.social bob.bsky.social
2121+2222+ # Use default fake test users (for testing only)
2323+ pnpm run db:seed
2424+2525+Options:
2626+ --help, -h Show this help message
2727+2828+Note:
2929+- Real user handles must exist in the database (users must have logged in)
3030+- Minimum 2 users required to create questions between them
3131+- Questions are local only and not synced to AT Protocol network
3232+`);
3333+ process.exit(0);
3434+}
3535+3636+const providedHandles = args.filter(arg => !arg.startsWith('--'));
3737+3838+// Default test users (fallback if no handles provided)
3939+const DEFAULT_TEST_USERS = [
4040+ {
4141+ did: "did:plc:test1234567890abcdef",
4242+ handle: "alice.test",
4343+ displayName: "Alice Test",
4444+ questionsOpen: true,
4545+ },
4646+ {
4747+ did: "did:plc:test0987654321fedcba",
4848+ handle: "bob.test",
4949+ displayName: "Bob Test",
5050+ questionsOpen: true,
5151+ },
5252+ {
5353+ did: "did:plc:testabcdef1234567890",
5454+ handle: "charlie.test",
5555+ displayName: "Charlie Test",
5656+ questionsOpen: true,
5757+ }
5858+];
5959+6060+async function getOrCreateTestUsers() {
6161+ if (providedHandles.length === 0) {
6262+ console.log("ℹ️ No handles provided, using default test users");
6363+ return DEFAULT_TEST_USERS;
6464+ }
6565+6666+ console.log(`ℹ️ Using provided handles: ${providedHandles.join(", ")}`);
6767+6868+ // Check if these users already exist in the database
6969+ const existingUsers = await db.select().from(users);
7070+ const existingHandles = new Set(existingUsers.map(u => u.handle));
7171+7272+ const testUsers = [];
7373+7474+ for (const handle of providedHandles) {
7575+ if (existingHandles.has(handle)) {
7676+ const existingUser = existingUsers.find(u => u.handle === handle);
7777+ testUsers.push({
7878+ did: existingUser!.did,
7979+ handle: existingUser!.handle,
8080+ displayName: existingUser!.displayName || handle,
8181+ questionsOpen: existingUser!.questionsOpen,
8282+ });
8383+ console.log(`✅ Found existing user: ${handle}`);
8484+ } else {
8585+ console.log(`⚠️ User ${handle} not found in database. You'll need to login as this user first, or use default test users.`);
8686+ }
8787+ }
8888+8989+ if (testUsers.length < 2) {
9090+ console.log("❌ Need at least 2 users to create questions between them.");
9191+ console.log("💡 Either:");
9292+ console.log(" 1. Login as real users first, then run: pnpm run db:seed handle1 handle2");
9393+ console.log(" 2. Use default test users: pnpm run db:seed");
9494+ process.exit(1);
9595+ }
9696+9797+ return testUsers;
9898+}
9999+100100+const QUESTION_TEMPLATES = [
101101+ {
102102+ content: "What's your favorite programming language and why?",
103103+ anonymous: false,
104104+ },
105105+ {
106106+ content: "How do you stay motivated when working on long-term projects?",
107107+ anonymous: false,
108108+ },
109109+ {
110110+ content: "What's the most interesting bug you've ever encountered?",
111111+ anonymous: false,
112112+ },
113113+ {
114114+ content: "If you could give advice to your past self when you started coding, what would it be?",
115115+ anonymous: false,
116116+ },
117117+ {
118118+ content: "What's your go-to method for debugging complex issues?",
119119+ anonymous: false,
120120+ },
121121+ {
122122+ content: "How do you balance learning new technologies with mastering existing ones?",
123123+ anonymous: false,
124124+ },
125125+ {
126126+ content: "What's the best piece of code you've ever written?",
127127+ anonymous: true,
128128+ },
129129+ {
130130+ content: "How do you handle imposter syndrome in tech?",
131131+ anonymous: true,
132132+ },
133133+ {
134134+ content: "What's your favorite development tool or IDE feature?",
135135+ anonymous: false,
136136+ },
137137+ {
138138+ content: "How do you approach code reviews?",
139139+ anonymous: false,
140140+ },
141141+];
142142+143143+function generateQuestions(testUsers: any[]) {
144144+ const questions = [];
145145+146146+ // Create questions between users in a round-robin fashion
147147+ for (let i = 0; i < QUESTION_TEMPLATES.length; i++) {
148148+ const template = QUESTION_TEMPLATES[i];
149149+ const authorIndex = i % testUsers.length;
150150+ const targetIndex = (i + 1) % testUsers.length;
151151+152152+ // Skip if author and target are the same (shouldn't happen with 2+ users)
153153+ if (authorIndex === targetIndex) continue;
154154+155155+ questions.push({
156156+ content: template.content,
157157+ authorHandle: testUsers[authorIndex].handle,
158158+ targetHandle: testUsers[targetIndex].handle,
159159+ anonymous: template.anonymous,
160160+ });
161161+ }
162162+163163+ return questions;
164164+}
165165+166166+async function seedTestData() {
167167+ console.log("🌱 Starting to seed test data...");
168168+169169+ try {
170170+ // Get or create test users based on provided handles
171171+ const testUsers = await getOrCreateTestUsers();
172172+173173+ // If using default test users, create them in the database
174174+ if (providedHandles.length === 0) {
175175+ console.log("📝 Creating default test users...");
176176+ for (const user of testUsers) {
177177+ try {
178178+ await db.insert(users).values(user).onConflictDoNothing();
179179+ console.log(`✅ User ${user.handle} created/exists`);
180180+ } catch (error) {
181181+ console.log(`⚠️ User ${user.handle} might already exist:`, error);
182182+ }
183183+ }
184184+ }
185185+186186+ // Create a map of handles to DIDs for easy lookup
187187+ const handleToDid = Object.fromEntries(
188188+ testUsers.map(user => [user.handle, user.did])
189189+ );
190190+191191+ // Generate questions based on available users
192192+ const questionsToCreate = generateQuestions(testUsers);
193193+194194+ // Insert test questions
195195+ console.log("❓ Creating test questions...");
196196+ for (const question of questionsToCreate) {
197197+ const authorDid = handleToDid[question.authorHandle];
198198+ const targetDid = handleToDid[question.targetHandle];
199199+200200+ if (!authorDid || !targetDid) {
201201+ console.log(`⚠️ Skipping question: missing DID for ${question.authorHandle} or ${question.targetHandle}`);
202202+ continue;
203203+ }
204204+205205+ await db.insert(questions).values({
206206+ content: question.content,
207207+ authorDid,
208208+ targetDid,
209209+ anonymous: question.anonymous || false,
210210+ sourceType: "askimut",
211211+ });
212212+213213+ const anonymousFlag = question.anonymous ? " (anonymous)" : "";
214214+ console.log(`✅ Question created: ${question.authorHandle} → ${question.targetHandle}${anonymousFlag}`);
215215+ }
216216+217217+ console.log("🎉 Test data seeding completed successfully!");
218218+219219+ // Show summary
220220+ const questionCount = await db.select().from(questions);
221221+ const userCount = await db.select().from(users);
222222+ console.log(`📊 Database now contains ${userCount.length} users and ${questionCount.length} questions`);
223223+224224+ } catch (error) {
225225+ console.error("❌ Error seeding test data:", error);
226226+ process.exit(1);
227227+ }
228228+}
229229+230230+// Run the seeding function
231231+seedTestData().then(() => {
232232+ console.log("✨ Seeding script finished");
233233+ process.exit(0);
234234+}).catch((error) => {
235235+ console.error("💥 Fatal error:", error);
236236+ process.exit(1);
237237+});