db reset and seed scripts

This commit is contained in:
Oliver Bryan
2026-01-08 17:53:56 +00:00
parent 528e41bd92
commit e02c53fc0c
6 changed files with 236 additions and 148 deletions

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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,
);
}
}
}
};