import * as sqlite3 from "sqlite3"; type User = { id: string; github_id: number; username: string; displayName: string; email: string; role: string | undefined; isPowerUser: boolean; }; type Printer = { id: string; name: string; description: string; status: number; }; type PrintJob = { id: string; printerId: string; userId: string; startAt: number; durationInMinutes: number; comments: string; aborted: boolean; abortReason: string | null; }; import { faker } from "@faker-js/faker"; import { random, sample } from "lodash"; import { DateTime } from "luxon"; import { open } from "sqlite"; import { v4 as uuidv4 } from "uuid"; const dbPath = "./db/sqlite.db"; // Configurations for test data generation const startDate = DateTime.fromISO("2024-11-01"); const endDate = DateTime.fromISO("2024-11-30"); const holidays: string[] = []; // Example holidays const existingJobs: PrintJob[] = []; // Calendar week usage configs const weekUsage = { 44: { minJobs: 4, maxJobs: 6 }, 45: { minJobs: 10, maxJobs: 15 }, 46: { minJobs: 10, maxJobs: 15 }, 47: { minJobs: 3, maxJobs: 5 }, 48: { minJobs: 10, maxJobs: 15 }, }; const printerUsageBias = { fastPrints: ["Printer A", "Printer B"], largePrints: ["Printer C", "Printer D", "Printer E"], }; const initDB = async () => { console.log("Initializing database connection..."); return open({ filename: dbPath, driver: sqlite3.Database, }); }; const createUser = (isPowerUser = false): User => { const name = [faker.person.firstName(), faker.person.lastName()]; const user = { id: uuidv4(), github_id: faker.number.int(), username: `${name[0].slice(0, 2)}${name[1].slice(0, 6)}`.toUpperCase(), displayName: `${name[0]} ${name[1]}`.toUpperCase(), email: `${name[0]}.${name[1]}@mercedes-benz.com`, role: sample(["user", "admin"]), isPowerUser, }; console.log("Created user:", user); return user; }; const createPrinter = (): Printer => { const printer = { id: uuidv4(), name: `Printer ${faker.number.int({ max: 9 })}`, description: faker.lorem.sentence(), status: random(0, 2), }; console.log("Created printer:", printer); return printer; }; const isPrinterAvailable = (printer: Printer, startAt: number, duration: number): boolean => { const endAt = startAt + duration * 60 * 1000; // Convert minutes to milliseconds return !existingJobs.some((job) => { const jobStart = job.startAt; const jobEnd = job.startAt + job.durationInMinutes * 60 * 1000; return ( printer.id === job.printerId && ((startAt >= jobStart && startAt < jobEnd) || (endAt > jobStart && endAt <= jobEnd) || (startAt <= jobStart && endAt >= jobEnd)) ); }); }; const weightedSampleUser = (users: User[]): User | undefined => { const weights = users.map((user) => (user.isPowerUser ? 3 : 1)); const weightedUsers = users.flatMap((user, index) => Array(weights[index]).fill(user)); return sample(weightedUsers); }; const normalRandom = (mean: number, stdDev: number): number => { let u = 0; let v = 0; while (u === 0) u = Math.random(); while (v === 0) v = Math.random(); return mean + stdDev * Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); }; const createPrintJob = (users: User[], printers: Printer[], startAt: number): PrintJob | null => { const user = weightedSampleUser(users) as User; let printer: Printer; // Probabilistic determination for large prints const isLargePrint = random(0, 100) < 30; // 30% chance for a large print if (isLargePrint) { printer = sample(printers.filter((p) => printerUsageBias.largePrints.includes(p.name))) as Printer; } else { printer = sample(printers.filter((p) => printerUsageBias.fastPrints.includes(p.name))) as Printer; } if (!printer) { printer = sample(printers) as Printer; } // Variable duration for print jobs with realistic limits let duration = Math.round(normalRandom(isLargePrint ? 240 : 75, 30)); const minDuration = 15; const maxDuration = isLargePrint ? 5760 : 3540; // Maximum duration of 96 hours or 59 minutes in minutes if (isLargePrint) { duration = Math.min(duration, maxDuration); } duration = Math.max(duration, minDuration); duration = Math.min(duration, maxDuration); // Ensure printer availability if (!isPrinterAvailable(printer, startAt, duration)) { console.log("Printer not available, skipping job creation."); return null; } // Dynamic error probability based on printer status, duration, and other factors let aborted = false; let abortReason = null; const baseErrorRate = Math.max(0, Math.min(5, 100)); // Ensure error rate is between 0% and 100% const userErrorModifier = user.isPowerUser ? -2 : 2; // Power users make fewer errors const timeErrorModifier = startAt >= DateTime.fromObject({ hour: 14 }).toMillis() && startAt <= DateTime.fromObject({ hour: 17 }).toMillis() ? 1 : 0; // More errors in the afternoon const errorRate = baseErrorRate + userErrorModifier + timeErrorModifier; if (random(0, 100) < Math.max(0, Math.min(errorRate, 100))) { aborted = true; abortReason = generateDynamicAbortReason(); } const printJob = { id: uuidv4(), printerId: printer.id, userId: user.id, startAt, durationInMinutes: duration, comments: faker.lorem.sentence(), aborted, abortReason, }; console.log("Created print job:", printJob); return printJob; }; const generateDynamicAbortReason = (): string => { const reasons = [ "Filament gerissen", "Drucker überhitzt", "Schichtversatz festgestellt", "Düse verstopft", "Kalibrierung fehlgeschlagen", `E${random(500, 599)}`, // Error codes ]; let reason = sample(reasons) as string; // Add typos to simulate human variability if (random(0, 1)) { reason = reason.replace("e", random(0, 1) ? "é" : "e"); } return reason; }; const generatePrintJobsForWeek = async ( users: User[], printers: Printer[], weekNumber: number, // biome-ignore lint/suspicious/noExplicitAny: db: any, dryRun: boolean, ) => { console.log(`Generating print jobs for week ${weekNumber}...`); const weekConfig = weekUsage[weekNumber as keyof typeof weekUsage]; const totalJobs = random(weekConfig.minJobs, weekConfig.maxJobs); const startOfWeek = startDate.plus({ weeks: weekNumber - 44 }); let jobsRemaining = totalJobs; for (let day = 0; day < 7; day++) { const dayDate = startOfWeek.plus({ days: day }); if (dayDate > endDate || jobsRemaining <= 0) break; if (holidays.includes(dayDate.toISODate() as string) || dayDate.weekday === 6 || dayDate.weekday === 7) { console.log(`Skipping holiday or weekend: ${dayDate.toISODate()}`); continue; } // Update printer status to simulate maintenance or breakdowns for (const printer of printers) { if (random(0, 100) < 5) { // 5% chance per day that a printer goes out of service printer.status = 2; // Status 2 means "out of service" console.log(`Printer ${printer.name} is out of service on ${dayDate.toISODate()}`); } else if (printer.status === 2) { printer.status = 0; // Printer becomes available again } } const jobsForDay = Math.min(jobsRemaining, random(1, 3)); jobsRemaining -= jobsForDay; console.log(`Generating ${jobsForDay} print jobs for day ${dayDate.toISODate()}...`); for (let i = 0; i < jobsForDay; i++) { // Simulate peak usage in the morning and after lunch const timeSlots = [ { hour: 7, minute: 0 }, { hour: 11, minute: 0 }, { hour: 13, minute: 0 }, { hour: 15, minute: 0 }, ]; const selectedSlot = sample(timeSlots) as { hour: number; minute: number; }; const startAt = DateTime.fromISO( `${dayDate.toISODate()}T${String(selectedSlot.hour).padStart(2, "0")}:${String(selectedSlot.minute + random(0, 30)).padStart(2, "0")}:00`, ); const printJob = createPrintJob(users, printers, startAt.toMillis()); if (printJob) { if (!dryRun) { await db.run( `INSERT INTO printJob (id, printerId, userId, startAt, durationInMinutes, comments, aborted, abortReason) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [ printJob.id, printJob.printerId, printJob.userId, printJob.startAt, printJob.durationInMinutes, printJob.comments, printJob.aborted ? 1 : 0, printJob.abortReason, ], ); } existingJobs.push(printJob); console.log("Inserted print job into database:", printJob.id); console.log( JSON.stringify({ event: "PrintJobCreated", jobId: printJob.id, printerId: printJob.printerId, userId: printJob.userId, startAt: new Date(printJob.startAt).toISOString(), duration: printJob.durationInMinutes, aborted: printJob.aborted, }), ); } } } }; const generateTestData = async (dryRun = false): Promise => { console.log("Starting test data generation..."); const db = await initDB(); // Generate users and printers const users = [ ...Array.from({ length: 7 }, () => createUser(false)), ...Array.from({ length: 3 }, () => createUser(true)), ]; const printers = Array.from({ length: 5 }, createPrinter); if (!dryRun) { // Insert users into the database for (const user of users) { await db.run( `INSERT INTO user (id, github_id, name, displayName, email, role) VALUES (?, ?, ?, ?, ?, ?)`, [user.id, user.github_id, user.username, user.displayName, user.email, user.role], ); console.log("Inserted user into database:", user.id); } // Insert printers into the database for (const printer of printers) { await db.run( `INSERT INTO printer (id, name, description, status) VALUES (?, ?, ?, ?)`, [printer.id, printer.name, printer.description, printer.status], ); console.log("Inserted printer into database:", printer.id); } } const validateData = (printJobs: PrintJob[], users: User[], printers: Printer[]): void => { for (const job of printJobs) { const userExists = users.some((user) => user.id === job.userId); const printerExists = printers.some((printer) => printer.id === job.printerId); if (!userExists || !printerExists) { console.error(`Invalid job detected: ${job.id}`); } } }; // Generate print jobs for each week for (const weekNumber of Object.keys(weekUsage) as unknown as number[]) { await generatePrintJobsForWeek(users, printers, Number.parseInt(`${weekNumber}`), db, dryRun); console.log(`======> 📅 Week ${weekNumber}`); validateData(existingJobs, users, printers); } if (!dryRun) { await db.close(); console.log("Database connection closed. Test data generation complete."); } else { console.log("Dry run complete. No data was written to the database."); } }; generateTestData(process.argv.includes("--dry-run")) .then(() => { console.log("Test data generation script finished."); }) .catch((err) => { console.error("Error generating test data:", err); });