-
Notifications
You must be signed in to change notification settings - Fork 0
[Frontend] - Add scripts for mock data initialization and database wiping #439
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
thomasdejaeghere
merged 4 commits into
development
from
410-frontend---add-scripts-for-mock-data-initialization-and-database-wiping
Apr 23, 2025
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
158200d
copied db scripts from student dashboard branch to this branch
rvdkeere 2e10444
implemented changes based on feedback
rvdkeere 5f29430
Merge branch 'development' into 410-frontend---add-scripts-for-mock-d…
rvdkeere c8d9a1b
Update seedDatabase.ts
rvdkeere File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| /** | ||
| * This function clears the database by truncating all tables and resetting their identities. | ||
| * It is useful for development and testing purposes, especially when you want to start with a clean slate. | ||
| * It is also used in the `runSeedDb.ts` file to clear the database before seeding it with test data. | ||
| * You can manually run this as a script using the following command: | ||
| * $ docker-compose exec backend npm run db:clear | ||
| */ | ||
|
|
||
|
|
||
| import { DataSource } from 'typeorm'; | ||
| import { DatasourceTypeORM } from '../src/infrastructure/database/data/data_sources/typeorm/datasourceTypeORM'; | ||
|
|
||
| export async function clearDatabase(): Promise<void> { | ||
| const connection: DataSource = await DatasourceTypeORM['datasourcePromise']; | ||
|
|
||
| const queryRunner = connection.createQueryRunner(); | ||
| await queryRunner.connect(); | ||
| await queryRunner.startTransaction(); | ||
|
|
||
| try { | ||
| const entities = connection.entityMetadatas; | ||
| for (const entity of entities) { | ||
| await connection.query(`TRUNCATE TABLE "${entity.tableName}" CASCADE`); | ||
| } | ||
|
|
||
| } catch (err) { | ||
| await queryRunner.rollbackTransaction(); | ||
| console.error('Error during DB clear:', err); | ||
| throw err; | ||
| } finally { | ||
| await queryRunner.release(); | ||
| } | ||
| } | ||
|
|
||
| // Run configuration (only executed when this file is run directly) | ||
| if (require.main === module) { | ||
| clearDatabase().catch((err) => { | ||
| console.error('Failed to clear database:', err); | ||
| process.exit(1); | ||
| }); | ||
| console.log('Database cleared successfully'); | ||
| process.exit(0); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,275 @@ | ||
| /** | ||
| * 🌱 Seed function: Random Database Seeder | ||
| * | ||
| * This function populates most database tables with random test data, | ||
| * including users, students, teachers, assignments, threads, and messages. | ||
| * | ||
| * ➤ Usage: | ||
| * You can run this file as a script to seed your database for demo purposes or manual testing. | ||
| * When ran directly, it will first clear the database before seeding. | ||
| * The exported function can also be useful for development and testing purposes, like E2E tests. | ||
| * The exported function does not clear the database, but this can be done with the `clearDatabase` function located in the same folder. | ||
| * | ||
| * To run it in the terminal: | ||
| * $ docker-compose exec backend npm run db:seed | ||
| * | ||
| * Note: Data generated is non-deterministic and meant for testing only. | ||
| */ | ||
|
|
||
|
|
||
| import { clearDatabase } from './clearDatabase'; | ||
| import bcrypt from "bcryptjs"; | ||
| import { faker } from '@faker-js/faker'; | ||
| import { Student } from '../src/core/entities/student' | ||
| import { Teacher } from '../src/core/entities/teacher' | ||
| import { Class } from '../src/core/entities/class' | ||
| import { Assignment } from "../src/core/entities/assignment"; | ||
| import { Group } from '../src/core/entities/group' | ||
| import { QuestionThread } from "../src/core/entities/questionThread"; | ||
| import { Message } from "../src/core/entities/message"; | ||
| import { StudentRepositoryTypeORM } from "../src/infrastructure/repositories/studentRepositoryTypeORM"; | ||
| import { TeacherRepositoryTypeORM } from "../src/infrastructure/repositories/teacherRepositoryTypeORM"; | ||
| import { ClassRepositoryTypeORM } from "../src/infrastructure/repositories/classRepositoryTypeORM"; | ||
| import { AssignmentRepositoryTypeORM } from "../src/infrastructure/repositories/assignmentRepositoryTypeORM"; | ||
| import { GroupRepositoryTypeORM } from "../src/infrastructure/repositories/groupRepositoryTypeORM"; | ||
| import { ThreadRepositoryTypeORM } from "../src/infrastructure/repositories/questionThreadRepositoryTypeORM"; | ||
| import { MessageRepositoryTypeORM } from "../src/infrastructure/repositories/messageRepositoryTypeORM"; | ||
| import { JoinRequestType } from "../src/core/entities/joinRequest"; | ||
| import { VisibilityType } from "../src/core/entities/questionThread"; | ||
|
|
||
| export async function seedDatabase(): Promise<void> { | ||
| const classRep = new ClassRepositoryTypeORM(); | ||
| const studentRep = new StudentRepositoryTypeORM(); | ||
| const teacherRep = new TeacherRepositoryTypeORM(); | ||
| const assignmentRep = new AssignmentRepositoryTypeORM(); | ||
| const groupRep = new GroupRepositoryTypeORM(); | ||
| const threadRep = new ThreadRepositoryTypeORM(); | ||
| const messageRep = new MessageRepositoryTypeORM(); | ||
|
|
||
| const teacherIds: string[] = []; | ||
| const classIds: string[] = []; | ||
| const studentIds: string[] = []; | ||
| const assignments: { id: string, classId: string }[] = []; | ||
|
|
||
| try { | ||
| // ── 1. Create Teachers ── | ||
| for (let i = 0; i < 5; i++) { | ||
| const firstName = faker.person.firstName(); | ||
| const lastName = faker.person.lastName(); | ||
| const email = `${i + 1}.teacher@ugent.be`; // known email address for login | ||
| const passwordHash = await bcrypt.hash('asdf1234', 10); // default password hash | ||
| const schoolName = faker.company.name(); | ||
|
|
||
| const teacherInput: Teacher = new Teacher( | ||
| email, | ||
| firstName, | ||
| lastName, | ||
| passwordHash, | ||
| schoolName, | ||
| ) | ||
|
|
||
| const savedTeacher = (await teacherRep.create(teacherInput)) as { id: string }; | ||
| teacherIds.push(savedTeacher.id); | ||
| } | ||
|
|
||
| // ── 2. Create Classes Per Teacher ── | ||
| for (const teacherId of teacherIds) { | ||
| for (let i = 0; i < 3; i++) { | ||
| const className = `${faker.word.adjective()} ${faker.word.noun()} Class`; | ||
| const description = faker.lorem.sentence(); | ||
| const targetAudience = `${faker.number.int({ min: 5, max: 12 })}th grade`; | ||
|
|
||
| const newClass = new Class(className, description, targetAudience, teacherId); | ||
| const createdClass = (await classRep.create(newClass)) as { id: string }; | ||
| classIds.push(createdClass.id); | ||
| } | ||
| } | ||
|
|
||
| // ── 3. Create Students ── | ||
| for (let i = 0; i < 20; i++) { | ||
| const firstName = faker.person.firstName(); | ||
| const lastName = faker.person.lastName(); | ||
| const email = `${i + 1}.student@ugent.be`; // known email for login | ||
| const passwordHash = await bcrypt.hash('asdf1234', 10); | ||
| const schoolName = faker.company.name(); | ||
|
|
||
| const studentInput: Student = new Student( | ||
| email, | ||
| firstName, | ||
| lastName, | ||
| passwordHash, | ||
| schoolName, | ||
| ) | ||
|
|
||
| const savedStudent = (await studentRep.create(studentInput)) as { id: string }; | ||
| studentIds.push(savedStudent.id); | ||
| // console.log(`Created student: ${firstName} ${lastName} email: ${email} with ID: ${savedStudent.id}`); | ||
| } | ||
|
|
||
| // ── 4. Add Students to Classes ── | ||
| // For each class, randomly add 7 students (or adjust the number as needed) | ||
| for (const classId of classIds) { | ||
| // faker.helpers.arrayElements picks a random subset from studentIds | ||
| const selectedStudents = faker.helpers.arrayElements(studentIds, 7); | ||
| // console.log(`Adding students to class ID ${classId}:`, selectedStudents); | ||
| for (const studentId of selectedStudents) { | ||
| await classRep.addUserToClass(classId, studentId, JoinRequestType.STUDENT); | ||
| // console.log(`Added student ID ${studentId} to class ID ${classId}`); | ||
| } | ||
| } | ||
|
|
||
| // ── 5. Create Assignments for Each Class ── | ||
| for (const classId of classIds) { | ||
| for (let i = 0; i < 3; i++) { | ||
| const learningPathId = "TODO"; // Replace with actual learning path ID once supported | ||
| // Choose a start date in the next 7 days | ||
| const startDate = faker.date.soon({ days: 7 }); | ||
| // Deadline is sometime 1 to 14 days after startDate | ||
| const additionalDays = faker.number.int({ min: 1, max: 14 }); | ||
| const deadline = new Date(startDate.getTime() + additionalDays * 24 * 60 * 60 * 1000); | ||
| const name = faker.lorem.sentence(3); // Random name for the assignment | ||
| const extraInstructions = faker.lorem.sentence(); | ||
|
|
||
| const assignment = new Assignment( | ||
| classId, | ||
| learningPathId, | ||
| startDate, | ||
| deadline, | ||
| name, | ||
| extraInstructions | ||
| ); | ||
|
|
||
| const savedAssignment = await assignmentRep.create(assignment) as { id: string }; | ||
| assignments.push({ id: savedAssignment.id, classId }); | ||
| } | ||
| } | ||
|
|
||
| // ── 6. Add Groups to Assignments ── | ||
| for (const { id: assignmentId, classId } of assignments) { | ||
| const students = await studentRep.getByClassId(classId); | ||
| const studentIds = students.map((s: any) => s.id); | ||
| const shuffled = faker.helpers.shuffle(studentIds); | ||
|
|
||
| const isSolo = faker.datatype.boolean(); // 50/50 solo vs group | ||
|
|
||
| if (isSolo) { | ||
| for (const id of shuffled) { | ||
| const group = new Group([id], assignmentId); | ||
| await groupRep.create(group); | ||
| } | ||
| } else { | ||
| const groupSize = faker.number.int({ min: 2, max: 3 }); | ||
| for (let i = 0; i < shuffled.length; i += groupSize) { | ||
| const members = shuffled.slice(i, i + groupSize); | ||
| const group = new Group(members, assignmentId); | ||
| await groupRep.create(group); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| // ── 7. Create Threads and Messages for Learning Steps ── | ||
| const visibilityOptions = [ | ||
| VisibilityType.PRIVATE, | ||
| VisibilityType.GROUP, | ||
| VisibilityType.PUBLIC | ||
| ]; | ||
|
|
||
| for (const { id: assignmentId, classId } of assignments) { | ||
| const students = await studentRep.getByClassId(classId); | ||
| const teachers = await teacherRep.getByClassId(classId); | ||
| const teacherIds = teachers.map(t => t.id); | ||
|
|
||
| // Select 5–6 students for threads | ||
| const selectedStudents = faker.helpers.arrayElements(students, { min: 5, max: 6 }); | ||
|
|
||
| for (const student of selectedStudents) { | ||
| const usedSteps = new Set<string>(); | ||
| const availableSteps = Array.from({ length: 5 }, (_, i) => `step-${i + 1}`) | ||
| .filter(stepId => !usedSteps.has(stepId)); | ||
|
|
||
| const threadCount = Math.min( | ||
| faker.number.int({ min: 1, max: 3 }), | ||
| availableSteps.length | ||
| ); | ||
|
|
||
| for (let j = 0; j < threadCount; j++) { | ||
| const stepId = faker.helpers.arrayElement(availableSteps); | ||
| usedSteps.add(stepId); // track it to avoid reuse | ||
|
|
||
| const visibility = faker.helpers.arrayElement(visibilityOptions); | ||
|
|
||
|
|
||
| const thread = new QuestionThread( | ||
| student.id!, | ||
| assignmentId, | ||
| stepId, | ||
| false, | ||
| visibility, | ||
| [] | ||
| ); | ||
|
|
||
| const savedThread = await threadRep.create(thread) as { id: string }; | ||
|
|
||
| // Create 1–2 messages from student and teacher | ||
| const studentMessage = new Message( | ||
| student.id!, | ||
| faker.date.recent({ days: 10 }), | ||
| savedThread.id!, | ||
| faker.lorem.sentence() | ||
| ); | ||
| const teacherMessage = new Message( | ||
| faker.helpers.arrayElement(teacherIds)!, | ||
| faker.date.recent({ days: 10 }), | ||
| savedThread.id!, | ||
| faker.lorem.sentence() | ||
| ); | ||
|
|
||
| await messageRep.create(studentMessage); | ||
| await messageRep.create(teacherMessage); | ||
| } | ||
| } | ||
|
|
||
| // Bonus: Add 2 public/global threads for this assignment | ||
| for (let k = 0; k < 2; k++) { | ||
| const globalThread = new QuestionThread( | ||
| faker.helpers.arrayElement(students).id!, | ||
| assignmentId, | ||
| `step-${faker.number.int({ min: 1, max: 5 })}`, | ||
| false, | ||
| VisibilityType.PUBLIC, | ||
| [] | ||
| ); | ||
| const savedThread = await threadRep.create(globalThread) as { id: string }; | ||
|
|
||
| const msg = new Message( | ||
| faker.helpers.arrayElement(teacherIds)!, | ||
| new Date(), | ||
| savedThread.id!, | ||
| faker.lorem.sentence() | ||
| ); | ||
| await messageRep.create(msg); | ||
| } | ||
| } | ||
| } catch (err) { | ||
| console.error('Error during DB seeding:', err); | ||
| throw err; | ||
| } | ||
| } | ||
|
|
||
| // Run configuration (only executed when this file is run directly) | ||
| if (require.main === module) { | ||
| clearDatabase().then(() => { | ||
| console.log('Database cleared successfully, now seeding...'); | ||
| seedDatabase().then(() => { | ||
| console.log('Database seeded successfully'); | ||
| process.exit(0); | ||
| }).catch((err) => { | ||
| console.error('Failed to seed database:', err); | ||
| process.exit(1); | ||
| }); | ||
| }).catch((err) => { | ||
| console.error('Failed to clear database:', err); | ||
| process.exit(1); | ||
| }); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice