mirror of
https://github.com/hex248/sprint.git
synced 2026-02-07 18:23:03 +00:00
db reset and seed scripts
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
"dev:frontend:tauri": "bun --filter @issue/frontend tauri",
|
"dev:frontend:tauri": "bun --filter @issue/frontend tauri",
|
||||||
"dev:backend": "bun --filter @issue/backend dev",
|
"dev:backend": "bun --filter @issue/backend dev",
|
||||||
"dev": "concurrently -n FRONTEND,BACKEND -c blue,green \"bun dev:frontend\" \"bun dev:backend\"",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
"db:start": "docker compose up -d",
|
"db:start": "docker compose up -d",
|
||||||
"db:stop": "docker compose down",
|
"db:stop": "docker compose down",
|
||||||
"db:migrate": "npx drizzle-kit generate && npx drizzle-kit migrate",
|
"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": {
|
"devDependencies": {
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
@@ -31,8 +33,5 @@
|
|||||||
"drizzle-orm": "^0.45.0",
|
"drizzle-orm": "^0.45.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"pg": "^8.16.3"
|
"pg": "^8.16.3"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"bun": ">=1.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
packages/backend/scripts/db-reset.ts
Normal file
48
packages/backend/scripts/db-reset.ts
Normal 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();
|
||||||
181
packages/backend/scripts/db-seed.ts
Normal file
181
packages/backend/scripts/db-seed.ts
Normal 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();
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import { User } from "@issue/shared";
|
|
||||||
import { withAuth, withCors } from "./auth/middleware";
|
import { withAuth, withCors } from "./auth/middleware";
|
||||||
import { db, testDB } from "./db/client";
|
import { testDB } from "./db/client";
|
||||||
import { routes } from "./routes";
|
import { routes } from "./routes";
|
||||||
import { createDemoData } from "./utils";
|
|
||||||
|
|
||||||
const DEV = process.argv.find((arg) => ["--dev", "--developer", "-d"].includes(arg.toLowerCase())) != null;
|
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;
|
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}`);
|
console.log(`eussi (issue server) listening on ${server.url}`);
|
||||||
await testDB();
|
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();
|
main();
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user