From e02c53fc0c7139b5816253f6aca74bac327009f6 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Thu, 8 Jan 2026 17:53:56 +0000 Subject: [PATCH] db reset and seed scripts --- package.json | 3 +- packages/backend/package.json | 9 +- packages/backend/scripts/db-reset.ts | 48 +++++++ packages/backend/scripts/db-seed.ts | 181 +++++++++++++++++++++++++++ packages/backend/src/index.ts | 13 +- packages/backend/src/utils.ts | 130 ------------------- 6 files changed, 236 insertions(+), 148 deletions(-) create mode 100644 packages/backend/scripts/db-reset.ts create mode 100644 packages/backend/scripts/db-seed.ts delete mode 100644 packages/backend/src/utils.ts diff --git a/package.json b/package.json index ac1812e..c9755e3 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dev:frontend:tauri": "bun --filter @issue/frontend tauri", "dev:backend": "bun --filter @issue/backend dev", "dev": "concurrently -n FRONTEND,BACKEND -c blue,green \"bun dev:frontend\" \"bun dev:backend\"", - "dev:desktop": "concurrently -n TAURI,BACKEND -c magenta,green \"bun dev:frontend:tauri\" \"bun dev:backend\"" + "dev:desktop": "concurrently -n TAURI,BACKEND -c magenta,green \"bun dev:frontend:tauri\" \"bun dev:backend\"", + "reset-and-seed": "bun --filter @issue/backend db:reset && bun --filter @issue/backend db:seed" } } diff --git a/packages/backend/package.json b/packages/backend/package.json index 0b822f4..0d224ce 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "type": "module", "engines": { - "bun": "1.3.4" + "bun": "1.3.4" }, "scripts": { "dev": "bun --watch src/index.ts --dev --PORT=3000", @@ -11,7 +11,9 @@ "db:start": "docker compose up -d", "db:stop": "docker compose down", "db:migrate": "npx drizzle-kit generate && npx drizzle-kit migrate", - "db:push": "npx drizzle-kit push" + "db:push": "npx drizzle-kit push", + "db:reset": "bun scripts/db-reset.ts", + "db:seed": "bun scripts/db-seed.ts" }, "devDependencies": { "@types/bcrypt": "^6.0.0", @@ -31,8 +33,5 @@ "drizzle-orm": "^0.45.0", "jsonwebtoken": "^9.0.3", "pg": "^8.16.3" - }, - "engines": { - "bun": ">=1.0.0" } } diff --git a/packages/backend/scripts/db-reset.ts b/packages/backend/scripts/db-reset.ts new file mode 100644 index 0000000..27fac52 --- /dev/null +++ b/packages/backend/scripts/db-reset.ts @@ -0,0 +1,48 @@ +import "dotenv/config"; +import { execSync } from "node:child_process"; +import { sql } from "drizzle-orm"; +import { drizzle } from "drizzle-orm/node-postgres"; + +const DATABASE_URL = process.env.DATABASE_URL; + +if (!DATABASE_URL) { + console.error("DATABASE_URL is not set"); + process.exit(1); +} + +const db = drizzle({ + connection: { + connectionString: DATABASE_URL, + }, +}); + +async function resetDatabase() { + console.log("resetting database..."); + + try { + // drop all tables in the correct order (respecting foreign key constraints) + await db.execute(sql`DROP TABLE IF EXISTS "Issue" CASCADE`); + await db.execute(sql`DROP TABLE IF EXISTS "Project" CASCADE`); + await db.execute(sql`DROP TABLE IF EXISTS "OrganisationMember" CASCADE`); + await db.execute(sql`DROP TABLE IF EXISTS "Organisation" CASCADE`); + await db.execute(sql`DROP TABLE IF EXISTS "User" CASCADE`); + + console.log("all tables dropped"); + + // push the schema to recreate tables + console.log("recreating schema..."); + execSync("npx drizzle-kit push --force", { + stdio: "inherit", + cwd: `${import.meta.dir}/..`, + }); + + console.log("database reset complete"); + } catch (error) { + console.error("failed to reset database:", error); + process.exit(1); + } + + process.exit(0); +} + +resetDatabase(); diff --git a/packages/backend/scripts/db-seed.ts b/packages/backend/scripts/db-seed.ts new file mode 100644 index 0000000..ba5395a --- /dev/null +++ b/packages/backend/scripts/db-seed.ts @@ -0,0 +1,181 @@ +import "dotenv/config"; +import { drizzle } from "drizzle-orm/node-postgres"; +import { Issue, Organisation, OrganisationMember, Project, User } from "@issue/shared"; +import bcrypt from "bcrypt"; + +const DATABASE_URL = process.env.DATABASE_URL; + +if (!DATABASE_URL) { + console.error("DATABASE_URL is not set"); + process.exit(1); +} + +const db = drizzle({ + connection: { + connectionString: DATABASE_URL, + }, +}); + +const hashPassword = (password: string) => bcrypt.hash(password, 10); + +const issueTitles = [ + "Fix login redirect loop", + "Add pagination to user list", + "Update dependencies to latest versions", + "Refactor authentication middleware", + "Add unit tests for payment service", + "Fix memory leak in websocket handler", + "Implement password reset flow", + "Add caching for API responses", + "Fix date formatting in reports", + "Add export to CSV feature", + "Improve error messages for form validation", + "Fix race condition in queue processor", + "Add dark mode support", + "Optimize database queries for dashboard", + "Fix broken image uploads on Safari", + "Add rate limiting to public endpoints", + "Implement user activity logging", + "Fix timezone handling in scheduler", + "Add search functionality to admin panel", + "Refactor legacy billing code", + "Fix email template rendering", + "Add webhook retry mechanism", + "Improve loading states across app", + "Fix notification preferences not saving", + "Add bulk delete for archived items", + "Fix SSO integration with Okta", + "Add two-factor authentication", + "Fix scroll position reset on navigation", + "Implement lazy loading for images", + "Add audit log for sensitive actions", + "Fix PDF generation timeout", + "Add keyboard shortcuts for common actions", +]; + +const issueDescriptions = [ + "Users are reporting this issue in production. Need to investigate and fix.", + "This has been requested by several customers. Should be straightforward to implement.", + "Low priority but would improve developer experience.", + "Blocking other work. Please prioritize.", + "Follow-up from the security audit.", + "Performance improvement that could reduce server costs.", + "Part of the Q1 roadmap.", + "Tech debt that we should address soon.", +]; + +async function seed() { + console.log("seeding database with demo data..."); + + try { + const passwordHash = await hashPassword("a"); + + // create 2 users + console.log("creating users..."); + const users = await db + .insert(User) + .values([ + { name: "user 1", username: "u1", passwordHash, avatarURL: null }, + { name: "user 2", username: "u2", passwordHash, avatarURL: null }, + ]) + .returning(); + + const u1 = users[0]!; + const u2 = users[1]!; + + console.log(`created ${users.length} users`); + + // create 2 orgs per user (4 total) + console.log("creating organisations..."); + const orgs = await db + .insert(Organisation) + .values([ + { name: "u1o1", slug: "u1o1", description: "User 1 organisation 1" }, + { name: "u1o2", slug: "u1o2", description: "User 1 organisation 2" }, + { name: "u2o1", slug: "u2o1", description: "User 2 organisation 1" }, + { name: "u2o2", slug: "u2o2", description: "User 2 organisation 2" }, + ]) + .returning(); + + const u1o1 = orgs[0]!; + const u1o2 = orgs[1]!; + const u2o1 = orgs[2]!; + const u2o2 = orgs[3]!; + + console.log(`created ${orgs.length} organisations`); + + // add members to organisations + console.log("adding organisation members..."); + await db.insert(OrganisationMember).values([ + { organisationId: u1o1.id, userId: u1.id, role: "owner" }, + { organisationId: u1o2.id, userId: u1.id, role: "owner" }, + { organisationId: u2o1.id, userId: u2.id, role: "owner" }, + { organisationId: u2o2.id, userId: u2.id, role: "owner" }, + ]); + + console.log("added organisation members"); + + // create 2 projects per org (8 total) + console.log("creating projects..."); + const projects = await db + .insert(Project) + .values([ + { key: "11P1", name: "u1o1p1", organisationId: u1o1.id, creatorId: u1.id }, + { key: "11P2", name: "u1o1p2", organisationId: u1o1.id, creatorId: u1.id }, + { key: "12P1", name: "u1o2p1", organisationId: u1o2.id, creatorId: u1.id }, + { key: "12P2", name: "u1o2p2", organisationId: u1o2.id, creatorId: u1.id }, + { key: "21P1", name: "u2o1p1", organisationId: u2o1.id, creatorId: u2.id }, + { key: "21P2", name: "u2o1p2", organisationId: u2o1.id, creatorId: u2.id }, + { key: "22P1", name: "u2o2p1", organisationId: u2o2.id, creatorId: u2.id }, + { key: "22P2", name: "u2o2p2", organisationId: u2o2.id, creatorId: u2.id }, + ]) + .returning(); + + console.log(`created ${projects.length} projects`); + + // create 0-4 issues per project + console.log("creating issues..."); + const allUsers = [u1, u2]; + const issueValues = []; + let issueTitleIndex = 0; + + for (const project of projects) { + const numIssues = Math.floor(Math.random() * 5); // 0-4 issues + for (let i = 1; i <= numIssues; i++) { + const creator = allUsers[Math.floor(Math.random() * allUsers.length)]!; + const assignee = + Math.random() > 0.3 ? allUsers[Math.floor(Math.random() * allUsers.length)] : null; + const title = issueTitles[issueTitleIndex % issueTitles.length]!; + const description = issueDescriptions[Math.floor(Math.random() * issueDescriptions.length)]!; + issueTitleIndex++; + + issueValues.push({ + projectId: project.id, + number: i, + title, + description, + creatorId: creator.id, + assigneeId: assignee?.id ?? null, + }); + } + } + + if (issueValues.length > 0) { + await db.insert(Issue).values(issueValues); + } + + console.log(`created ${issueValues.length} issues`); + + console.log("database seeding complete"); + console.log("\ndemo accounts (password: a):"); + console.log(" - u1"); + console.log(" - u2"); + } catch (error) { + console.error("failed to seed database:", error); + process.exit(1); + } + + process.exit(0); +} + +seed(); diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 82c329b..6943709 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,8 +1,6 @@ -import { User } from "@issue/shared"; import { withAuth, withCors } from "./auth/middleware"; -import { db, testDB } from "./db/client"; +import { testDB } from "./db/client"; import { routes } from "./routes"; -import { createDemoData } from "./utils"; const DEV = process.argv.find((arg) => ["--dev", "--developer", "-d"].includes(arg.toLowerCase())) != null; const PORT = process.argv.find((arg) => arg.toLowerCase().startsWith("--port="))?.split("=")[1] || 0; @@ -54,15 +52,6 @@ const main = async () => { console.log(`eussi (issue server) listening on ${server.url}`); await testDB(); - - if (DEV) { - const users = await db.select().from(User); - if (users.length === 0) { - console.log("creating demo data..."); - await createDemoData(); - console.log("demo data created"); - } - } }; main(); diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts deleted file mode 100644 index a8a1633..0000000 --- a/packages/backend/src/utils.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { hashPassword } from "./auth/utils"; -import { - createIssue, - createOrganisation, - createOrganisationMember, - createProject, - createUser, -} from "./db/queries"; - -export const createDemoData = async () => { - const passwordHash = await hashPassword("a"); - - // create two users - const user1 = await createUser("test 1", "t1", passwordHash); - if (!user1) { - throw new Error("failed to create test 1"); - } - - const user2 = await createUser("test 2", "t2", passwordHash); - if (!user2) { - throw new Error("failed to create test 2"); - } - - // create four organisations - const organisations = []; - for (let i = 1; i <= 4; i++) { - const org = await createOrganisation( - `Demo Organisation ${i}`, - `demo-org-${i}`, - `A demo organisation ${i} for testing`, - ); - if (!org) { - throw new Error(`failed to create demo organisation ${i}`); - } - organisations.push(org); - } - - // set up organisation memberships - // user 1 owns: org 1, org 2; has access to: org 3 - // user 2 owns: org 3, org 4; has access to: org 2 - const org1 = organisations[0]; - const org2 = organisations[1]; - const org3 = organisations[2]; - const org4 = organisations[3]; - if (!org1 || !org2 || !org3 || !org4) { - throw new Error("failed to create organisations"); - } - - // user 1 memberships - await createOrganisationMember(org1.id, user1.id, "owner"); // owns org 1 - await createOrganisationMember(org2.id, user1.id, "owner"); // owns org 2 - await createOrganisationMember(org3.id, user1.id, "member"); // member of org 3 - - // user 2 memberships - await createOrganisationMember(org2.id, user2.id, "member"); // member of org 2 - await createOrganisationMember(org3.id, user2.id, "owner"); // owns org 3 - await createOrganisationMember(org4.id, user2.id, "owner"); // owns org 4 - - // project names: AAAAA, BBBBB, CCCCC, DDDDD, EEEEE, FFFFF, GGGGG, HHHHH, IIIII, JJJJJ, KKKKK, LLLLL - const projectNames = [ - "AAAAA", - "BBBBB", - "CCCCC", - "DDDDD", - "EEEEE", - "FFFFF", - "GGGGG", - "HHHHH", - "IIIII", - "JJJJJ", - "KKKKK", - "LLLLL", - ]; - - // create 3 projects per organisation - let projectIndex = 0; - const orgConfigs = [ - { org: org1, creator: user1 }, // org 1: user1 - { org: org2, creator: user1 }, // org 2: user1 - { org: org3, creator: user2 }, // org 3: user2 - { org: org4, creator: user2 }, // org 4: user2 - ]; - - for (const config of orgConfigs) { - for (let projNum = 0; projNum < 3; projNum++) { - const projectName = projectNames[projectIndex++]; - if (!projectName) { - throw new Error("ran out of project names"); - } - - const project = await createProject( - projectName.slice(0, 4), - projectName, - config.creator.id, - config.org.id, - ); - if (!project) { - throw new Error(`failed to create demo project: ${projectName}`); - } - - // create some issues for each project - for (let i = 1; i <= 3; i++) { - let assignee: number | undefined; - - // for crossover organizations (org2 and org3), randomly assign to either user - if (config.org.id === org2.id || config.org.id === org3.id) { - // 40% chance to assign to creator, 40% chance to assign to other user, 20% chance unassigned - const rand = Math.random(); - if (rand < 0.4) { - assignee = config.creator.id; - } else if (rand < 0.8) { - assignee = config.org.id === org2.id ? user2.id : user1.id; // other user - } - // else: undefined (unassigned) - } else { - // for exclusive organizations (org1 and org4), assign to creator on even issues - assignee = i % 2 === 0 ? config.creator.id : undefined; - } - - await createIssue( - project.id, - `Issue ${i} in ${projectName}`, - `This is a description for issue ${i} in ${projectName}.`, - config.creator.id, - assignee, - ); - } - } - } -};