Merge pull request #15 from SELab-2/chore/database-setup
chore(backend): Database setup
This commit is contained in:
		
						commit
						44a433e8c9
					
				
					 47 changed files with 2634 additions and 46 deletions
				
			
		
							
								
								
									
										6
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -43,6 +43,9 @@ build/Release | ||||||
| node_modules/ | node_modules/ | ||||||
| jspm_packages/ | jspm_packages/ | ||||||
| 
 | 
 | ||||||
|  | # package-lock.json | ||||||
|  | backend/package-lock.json | ||||||
|  | 
 | ||||||
| # Snowpack dependency directory (https://snowpack.dev/) | # Snowpack dependency directory (https://snowpack.dev/) | ||||||
| web_modules/ | web_modules/ | ||||||
| 
 | 
 | ||||||
|  | @ -641,7 +644,8 @@ FodyWeavers.xsd | ||||||
| .LSOverride | .LSOverride | ||||||
| 
 | 
 | ||||||
| # Icon must end with two \r | # Icon must end with two \r | ||||||
| Icon | Icon | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| # Thumbnails | # Thumbnails | ||||||
| ._* | ._* | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								backend/.env.development.example
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								backend/.env.development.example
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | DWENGO_PORT=3000 | ||||||
|  | DWENGO_DB_HOST=localhost | ||||||
|  | DWENGO_DB_PORT=5431 | ||||||
|  | DWENGO_DB_USERNAME=postgres | ||||||
|  | DWENGO_DB_PASSWORD=postgres | ||||||
|  | DWENGO_DB_UPDATE=true | ||||||
							
								
								
									
										3
									
								
								backend/.env.test
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								backend/.env.test
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | PORT=3000 | ||||||
|  | DWENGO_DB_UPDATE=true | ||||||
|  | DWENGO_DB_NAME=":memory:" | ||||||
|  | @ -1,2 +0,0 @@ | ||||||
| -- Create the database |  | ||||||
| CREATE DATABASE dwengo; |  | ||||||
|  | @ -11,18 +11,19 @@ | ||||||
|         "format": "prettier --write src/", |         "format": "prettier --write src/", | ||||||
|         "format-check": "prettier --check src/", |         "format-check": "prettier --check src/", | ||||||
|         "lint": "eslint . --fix", |         "lint": "eslint . --fix", | ||||||
|         "test:unit": "vitest --run" |         "test:unit": "vitest" | ||||||
|     }, |     }, | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|         "@mikro-orm/core": "^6.4.6", |         "@mikro-orm/core": "6.4.6", | ||||||
|         "@mikro-orm/postgresql": "^6.4.6", |         "@mikro-orm/postgresql": "6.4.6", | ||||||
|         "@mikro-orm/reflection": "^6.4.6", |         "@mikro-orm/sqlite": "6.4.6", | ||||||
|         "@types/js-yaml": "^4.0.9", |         "@mikro-orm/reflection": "6.4.6", | ||||||
|         "@mikro-orm/sqlite": "^6.4.6", |  | ||||||
|         "@mikro-orm/reflection": "^6.4.6", |  | ||||||
|         "dotenv": "^16.4.7", |         "dotenv": "^16.4.7", | ||||||
|         "express": "^5.0.1", |         "express": "^5.0.1", | ||||||
|         "js-yaml": "^4.1.0" |         "uuid": "^11.1.0", | ||||||
|  |         "express": "^5.0.1", | ||||||
|  |         "js-yaml": "^4.1.0", | ||||||
|  |         "@types/js-yaml": "^4.0.9", | ||||||
|     }, |     }, | ||||||
|     "devDependencies": { |     "devDependencies": { | ||||||
|         "@mikro-orm/cli": "^6.4.6", |         "@mikro-orm/cli": "^6.4.6", | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| import express, { Express, Response } from 'express'; | import express, { Express, Response } from 'express'; | ||||||
| import initORM from './orm.js'; | import { initORM } from './orm.js'; | ||||||
|  | import { EnvVars, getNumericEnvVar } from './util/envvars.js'; | ||||||
|  | 
 | ||||||
| import themeRoutes from './routes/themes.js'; | import themeRoutes from './routes/themes.js'; | ||||||
| 
 | 
 | ||||||
| import studentRouter from './routes/student'; | import studentRouter from './routes/student'; | ||||||
|  | @ -11,7 +13,7 @@ import questionRouter from './routes/question'; | ||||||
| import loginRouter from './routes/login'; | import loginRouter from './routes/login'; | ||||||
| 
 | 
 | ||||||
| const app: Express = express(); | const app: Express = express(); | ||||||
| const port: string | number = process.env.PORT || 3000; | const port: string | number = getNumericEnvVar(EnvVars.Port); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| // TODO Replace with Express routes
 | // TODO Replace with Express routes
 | ||||||
|  | @ -39,4 +41,4 @@ async function startServer() { | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| startServer(); | await startServer(); | ||||||
|  |  | ||||||
							
								
								
									
										18
									
								
								backend/src/data/assignments/assignment-repository.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								backend/src/data/assignments/assignment-repository.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | ||||||
|  | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
|  | import { Assignment } from '../../entities/assignments/assignment.entity.js'; | ||||||
|  | import { Class } from '../../entities/classes/class.entity.js'; | ||||||
|  | 
 | ||||||
|  | export class AssignmentRepository extends DwengoEntityRepository<Assignment> { | ||||||
|  |     public findByClassAndId( | ||||||
|  |         within: Class, | ||||||
|  |         id: number | ||||||
|  |     ): Promise<Assignment | null> { | ||||||
|  |         return this.findOne({ within: within, id: id }); | ||||||
|  |     } | ||||||
|  |     public findAllAssignmentsInClass(within: Class): Promise<Assignment[]> { | ||||||
|  |         return this.findAll({ where: { within: within } }); | ||||||
|  |     } | ||||||
|  |     public deleteByClassAndId(within: Class, id: number): Promise<void> { | ||||||
|  |         return this.deleteWhere({ within: within, id: id }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										29
									
								
								backend/src/data/assignments/group-repository.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								backend/src/data/assignments/group-repository.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | ||||||
|  | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
|  | import { Group } from '../../entities/assignments/group.entity.js'; | ||||||
|  | import { Assignment } from '../../entities/assignments/assignment.entity.js'; | ||||||
|  | 
 | ||||||
|  | export class GroupRepository extends DwengoEntityRepository<Group> { | ||||||
|  |     public findByAssignmentAndGroupNumber( | ||||||
|  |         assignment: Assignment, | ||||||
|  |         groupNumber: number | ||||||
|  |     ): Promise<Group | null> { | ||||||
|  |         return this.findOne({ | ||||||
|  |             assignment: assignment, | ||||||
|  |             groupNumber: groupNumber, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     public findAllGroupsForAssignment( | ||||||
|  |         assignment: Assignment | ||||||
|  |     ): Promise<Group[]> { | ||||||
|  |         return this.findAll({ where: { assignment: assignment } }); | ||||||
|  |     } | ||||||
|  |     public deleteByAssignmentAndGroupNumber( | ||||||
|  |         assignment: Assignment, | ||||||
|  |         groupNumber: number | ||||||
|  |     ) { | ||||||
|  |         return this.deleteWhere({ | ||||||
|  |             assignment: assignment, | ||||||
|  |             groupNumber: groupNumber, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								backend/src/data/assignments/submission-repository.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								backend/src/data/assignments/submission-repository.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,61 @@ | ||||||
|  | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
|  | import { Group } from '../../entities/assignments/group.entity.js'; | ||||||
|  | import { Submission } from '../../entities/assignments/submission.entity.js'; | ||||||
|  | import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; | ||||||
|  | import { Student } from '../../entities/users/student.entity.js'; | ||||||
|  | 
 | ||||||
|  | export class SubmissionRepository extends DwengoEntityRepository<Submission> { | ||||||
|  |     public findSubmissionByLearningObjectAndSubmissionNumber( | ||||||
|  |         loId: LearningObjectIdentifier, | ||||||
|  |         submissionNumber: number | ||||||
|  |     ): Promise<Submission | null> { | ||||||
|  |         return this.findOne({ | ||||||
|  |             learningObjectHruid: loId.hruid, | ||||||
|  |             learningObjectLanguage: loId.language, | ||||||
|  |             learningObjectVersion: loId.version, | ||||||
|  |             submissionNumber: submissionNumber, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public findMostRecentSubmissionForStudent( | ||||||
|  |         loId: LearningObjectIdentifier, | ||||||
|  |         submitter: Student | ||||||
|  |     ): Promise<Submission | null> { | ||||||
|  |         return this.findOne( | ||||||
|  |             { | ||||||
|  |                 learningObjectHruid: loId.hruid, | ||||||
|  |                 learningObjectLanguage: loId.language, | ||||||
|  |                 learningObjectVersion: loId.version, | ||||||
|  |                 submitter: submitter, | ||||||
|  |             }, | ||||||
|  |             { orderBy: { submissionNumber: 'DESC' } } | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public findMostRecentSubmissionForGroup( | ||||||
|  |         loId: LearningObjectIdentifier, | ||||||
|  |         group: Group | ||||||
|  |     ): Promise<Submission | null> { | ||||||
|  |         return this.findOne( | ||||||
|  |             { | ||||||
|  |                 learningObjectHruid: loId.hruid, | ||||||
|  |                 learningObjectLanguage: loId.language, | ||||||
|  |                 learningObjectVersion: loId.version, | ||||||
|  |                 onBehalfOf: group, | ||||||
|  |             }, | ||||||
|  |             { orderBy: { submissionNumber: 'DESC' } } | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public deleteSubmissionByLearningObjectAndSubmissionNumber( | ||||||
|  |         loId: LearningObjectIdentifier, | ||||||
|  |         submissionNumber: number | ||||||
|  |     ): Promise<void> { | ||||||
|  |         return this.deleteWhere({ | ||||||
|  |             learningObjectHruid: loId.hruid, | ||||||
|  |             learningObjectLanguage: loId.language, | ||||||
|  |             learningObjectVersion: loId.version, | ||||||
|  |             submissionNumber: submissionNumber, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								backend/src/data/classes/class-join-request-repository.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								backend/src/data/classes/class-join-request-repository.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
|  | import { Class } from '../../entities/classes/class.entity.js'; | ||||||
|  | import { ClassJoinRequest } from '../../entities/classes/class-join-request.entity.js'; | ||||||
|  | import { Student } from '../../entities/users/student.entity.js'; | ||||||
|  | 
 | ||||||
|  | export class ClassJoinRequestRepository extends DwengoEntityRepository<ClassJoinRequest> { | ||||||
|  |     public findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> { | ||||||
|  |         return this.findAll({ where: { requester: requester } }); | ||||||
|  |     } | ||||||
|  |     public findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> { | ||||||
|  |         return this.findAll({ where: { class: clazz } }); | ||||||
|  |     } | ||||||
|  |     public deleteBy(requester: Student, clazz: Class): Promise<void> { | ||||||
|  |         return this.deleteWhere({ requester: requester, class: clazz }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								backend/src/data/classes/class-repository.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								backend/src/data/classes/class-repository.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
|  | import { Class } from '../../entities/classes/class.entity.js'; | ||||||
|  | 
 | ||||||
|  | export class ClassRepository extends DwengoEntityRepository<Class> { | ||||||
|  |     public findById(id: string): Promise<Class | null> { | ||||||
|  |         return this.findOne({ classId: id }); | ||||||
|  |     } | ||||||
|  |     public deleteById(id: string): Promise<void> { | ||||||
|  |         return this.deleteWhere({ classId: id }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										31
									
								
								backend/src/data/classes/teacher-invitation-repository.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								backend/src/data/classes/teacher-invitation-repository.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | ||||||
|  | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
|  | import { Class } from '../../entities/classes/class.entity.js'; | ||||||
|  | import { TeacherInvitation } from '../../entities/classes/teacher-invitation.entity.js'; | ||||||
|  | import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||||
|  | 
 | ||||||
|  | export class TeacherInvitationRepository extends DwengoEntityRepository<TeacherInvitation> { | ||||||
|  |     public findAllInvitationsForClass( | ||||||
|  |         clazz: Class | ||||||
|  |     ): Promise<TeacherInvitation[]> { | ||||||
|  |         return this.findAll({ where: { class: clazz } }); | ||||||
|  |     } | ||||||
|  |     public findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> { | ||||||
|  |         return this.findAll({ where: { sender: sender } }); | ||||||
|  |     } | ||||||
|  |     public findAllInvitationsFor( | ||||||
|  |         receiver: Teacher | ||||||
|  |     ): Promise<TeacherInvitation[]> { | ||||||
|  |         return this.findAll({ where: { receiver: receiver } }); | ||||||
|  |     } | ||||||
|  |     public deleteBy( | ||||||
|  |         clazz: Class, | ||||||
|  |         sender: Teacher, | ||||||
|  |         receiver: Teacher | ||||||
|  |     ): Promise<void> { | ||||||
|  |         return this.deleteWhere({ | ||||||
|  |             sender: sender, | ||||||
|  |             receiver: receiver, | ||||||
|  |             class: clazz, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								backend/src/data/content/attachment-repository.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								backend/src/data/content/attachment-repository.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
|  | import { Attachment } from '../../entities/content/attachment.entity.js'; | ||||||
|  | import { LearningObject } from '../../entities/content/learning-object.entity.js'; | ||||||
|  | 
 | ||||||
|  | export class AttachmentRepository extends DwengoEntityRepository<Attachment> { | ||||||
|  |     public findByLearningObjectAndNumber( | ||||||
|  |         learningObject: LearningObject, | ||||||
|  |         sequenceNumber: number | ||||||
|  |     ) { | ||||||
|  |         return this.findOne({ | ||||||
|  |             learningObject: learningObject, | ||||||
|  |             sequenceNumber: sequenceNumber, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     // This repository is read-only for now since creating own learning object is an extension feature.
 | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								backend/src/data/content/learning-object-repository.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								backend/src/data/content/learning-object-repository.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
|  | import { LearningObject } from '../../entities/content/learning-object.entity.js'; | ||||||
|  | import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; | ||||||
|  | 
 | ||||||
|  | export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> { | ||||||
|  |     public findByIdentifier( | ||||||
|  |         identifier: LearningObjectIdentifier | ||||||
|  |     ): Promise<LearningObject | null> { | ||||||
|  |         return this.findOne({ | ||||||
|  |             hruid: identifier.hruid, | ||||||
|  |             language: identifier.language, | ||||||
|  |             version: identifier.version, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     // This repository is read-only for now since creating own learning object is an extension feature.
 | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								backend/src/data/content/learning-path-repository.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								backend/src/data/content/learning-path-repository.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
|  | import { LearningPath } from '../../entities/content/learning-path.entity.js'; | ||||||
|  | import { Language } from '../../entities/content/language.js'; | ||||||
|  | 
 | ||||||
|  | export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { | ||||||
|  |     public findByHruidAndLanguage( | ||||||
|  |         hruid: string, | ||||||
|  |         language: Language | ||||||
|  |     ): Promise<LearningPath | null> { | ||||||
|  |         return this.findOne({ hruid: hruid, language: language }); | ||||||
|  |     } | ||||||
|  |     // This repository is read-only for now since creating own learning object is an extension feature.
 | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								backend/src/data/dwengo-entity-repository.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								backend/src/data/dwengo-entity-repository.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | ||||||
|  | import { EntityRepository, FilterQuery } from '@mikro-orm/core'; | ||||||
|  | 
 | ||||||
|  | export abstract class DwengoEntityRepository< | ||||||
|  |     T extends object, | ||||||
|  | > extends EntityRepository<T> { | ||||||
|  |     public async save(entity: T) { | ||||||
|  |         let em = this.getEntityManager(); | ||||||
|  |         em.persist(entity); | ||||||
|  |         await em.flush(); | ||||||
|  |     } | ||||||
|  |     public async deleteWhere(query: FilterQuery<T>) { | ||||||
|  |         let toDelete = await this.findOne(query); | ||||||
|  |         let em = this.getEntityManager(); | ||||||
|  |         if (toDelete) { | ||||||
|  |             em.remove(toDelete); | ||||||
|  |             await em.flush(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										33
									
								
								backend/src/data/questions/answer-repository.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								backend/src/data/questions/answer-repository.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | ||||||
|  | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
|  | import { Answer } from '../../entities/questions/answer.entity.js'; | ||||||
|  | import { Question } from '../../entities/questions/question.entity.js'; | ||||||
|  | import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||||
|  | 
 | ||||||
|  | export class AnswerRepository extends DwengoEntityRepository<Answer> { | ||||||
|  |     public createAnswer(answer: { | ||||||
|  |         toQuestion: Question; | ||||||
|  |         author: Teacher; | ||||||
|  |         content: string; | ||||||
|  |     }): Promise<Answer> { | ||||||
|  |         let answerEntity = new Answer(); | ||||||
|  |         answerEntity.toQuestion = answer.toQuestion; | ||||||
|  |         answerEntity.author = answer.author; | ||||||
|  |         answerEntity.content = answer.content; | ||||||
|  |         return this.insert(answerEntity); | ||||||
|  |     } | ||||||
|  |     public findAllAnswersToQuestion(question: Question): Promise<Answer[]> { | ||||||
|  |         return this.findAll({ | ||||||
|  |             where: { toQuestion: question }, | ||||||
|  |             orderBy: { sequenceNumber: 'ASC' }, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     public removeAnswerByQuestionAndSequenceNumber( | ||||||
|  |         question: Question, | ||||||
|  |         sequenceNumber: number | ||||||
|  |     ): Promise<void> { | ||||||
|  |         return this.deleteWhere({ | ||||||
|  |             toQuestion: question, | ||||||
|  |             sequenceNumber: sequenceNumber, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										45
									
								
								backend/src/data/questions/question-repository.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								backend/src/data/questions/question-repository.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | ||||||
|  | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
|  | import { Question } from '../../entities/questions/question.entity.js'; | ||||||
|  | import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; | ||||||
|  | import { Student } from '../../entities/users/student.entity.js'; | ||||||
|  | 
 | ||||||
|  | export class QuestionRepository extends DwengoEntityRepository<Question> { | ||||||
|  |     public createQuestion(question: { | ||||||
|  |         loId: LearningObjectIdentifier; | ||||||
|  |         author: Student; | ||||||
|  |         content: string; | ||||||
|  |     }): Promise<Question> { | ||||||
|  |         let questionEntity = new Question(); | ||||||
|  |         questionEntity.learningObjectHruid = question.loId.hruid; | ||||||
|  |         questionEntity.learningObjectLanguage = question.loId.language; | ||||||
|  |         questionEntity.learningObjectVersion = question.loId.version; | ||||||
|  |         questionEntity.author = question.author; | ||||||
|  |         questionEntity.content = question.content; | ||||||
|  |         return this.insert(questionEntity); | ||||||
|  |     } | ||||||
|  |     public findAllQuestionsAboutLearningObject( | ||||||
|  |         loId: LearningObjectIdentifier | ||||||
|  |     ): Promise<Question[]> { | ||||||
|  |         return this.findAll({ | ||||||
|  |             where: { | ||||||
|  |                 learningObjectHruid: loId.hruid, | ||||||
|  |                 learningObjectLanguage: loId.language, | ||||||
|  |                 learningObjectVersion: loId.version, | ||||||
|  |             }, | ||||||
|  |             orderBy: { | ||||||
|  |                 sequenceNumber: 'ASC', | ||||||
|  |             }, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     public removeQuestionByLearningObjectAndSequenceNumber( | ||||||
|  |         loId: LearningObjectIdentifier, | ||||||
|  |         sequenceNumber: number | ||||||
|  |     ): Promise<void> { | ||||||
|  |         return this.deleteWhere({ | ||||||
|  |             learningObjectHruid: loId.hruid, | ||||||
|  |             learningObjectLanguage: loId.language, | ||||||
|  |             learningObjectVersion: loId.version, | ||||||
|  |             sequenceNumber: sequenceNumber, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										119
									
								
								backend/src/data/repositories.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								backend/src/data/repositories.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,119 @@ | ||||||
|  | import { | ||||||
|  |     AnyEntity, | ||||||
|  |     EntityManager, | ||||||
|  |     EntityName, | ||||||
|  |     EntityRepository, | ||||||
|  | } from '@mikro-orm/core'; | ||||||
|  | import { forkEntityManager } from '../orm.js'; | ||||||
|  | import { StudentRepository } from './users/student-repository.js'; | ||||||
|  | import { Student } from '../entities/users/student.entity.js'; | ||||||
|  | import { User } from '../entities/users/user.entity.js'; | ||||||
|  | import { UserRepository } from './users/user-repository.js'; | ||||||
|  | import { Teacher } from '../entities/users/teacher.entity.js'; | ||||||
|  | import { TeacherRepository } from './users/teacher-repository.js'; | ||||||
|  | import { Class } from '../entities/classes/class.entity.js'; | ||||||
|  | import { ClassRepository } from './classes/class-repository.js'; | ||||||
|  | import { ClassJoinRequest } from '../entities/classes/class-join-request.entity.js'; | ||||||
|  | import { ClassJoinRequestRepository } from './classes/class-join-request-repository.js'; | ||||||
|  | import { TeacherInvitationRepository } from './classes/teacher-invitation-repository.js'; | ||||||
|  | import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js'; | ||||||
|  | import { Assignment } from '../entities/assignments/assignment.entity.js'; | ||||||
|  | import { AssignmentRepository } from './assignments/assignment-repository.js'; | ||||||
|  | import { GroupRepository } from './assignments/group-repository.js'; | ||||||
|  | import { Group } from '../entities/assignments/group.entity.js'; | ||||||
|  | import { Submission } from '../entities/assignments/submission.entity.js'; | ||||||
|  | import { SubmissionRepository } from './assignments/submission-repository.js'; | ||||||
|  | import { Question } from '../entities/questions/question.entity.js'; | ||||||
|  | import { QuestionRepository } from './questions/question-repository.js'; | ||||||
|  | import { Answer } from '../entities/questions/answer.entity.js'; | ||||||
|  | import { AnswerRepository } from './questions/answer-repository.js'; | ||||||
|  | import { LearningObject } from '../entities/content/learning-object.entity.js'; | ||||||
|  | import { LearningObjectRepository } from './content/learning-object-repository.js'; | ||||||
|  | import { LearningPath } from '../entities/content/learning-path.entity.js'; | ||||||
|  | import { LearningPathRepository } from './content/learning-path-repository.js'; | ||||||
|  | import { AttachmentRepository } from './content/attachment-repository.js'; | ||||||
|  | import { Attachment } from '../entities/content/attachment.entity.js'; | ||||||
|  | 
 | ||||||
|  | let entityManager: EntityManager | undefined; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Execute all the database operations within the function f in a single transaction. | ||||||
|  |  */ | ||||||
|  | export function transactional<T>(f: () => Promise<T>) { | ||||||
|  |     entityManager?.transactional(f); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>( | ||||||
|  |     entity: EntityName<T> | ||||||
|  | ): () => R { | ||||||
|  |     let cachedRepo: R | undefined; | ||||||
|  |     return (): R => { | ||||||
|  |         if (!cachedRepo) { | ||||||
|  |             if (!entityManager) { | ||||||
|  |                 entityManager = forkEntityManager(); | ||||||
|  |             } | ||||||
|  |             cachedRepo = entityManager.getRepository(entity) as R; | ||||||
|  |         } | ||||||
|  |         return cachedRepo; | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* Users */ | ||||||
|  | export const getUserRepository = repositoryGetter<User, UserRepository>(User); | ||||||
|  | export const getStudentRepository = repositoryGetter< | ||||||
|  |     Student, | ||||||
|  |     StudentRepository | ||||||
|  | >(Student); | ||||||
|  | export const getTeacherRepository = repositoryGetter< | ||||||
|  |     Teacher, | ||||||
|  |     TeacherRepository | ||||||
|  | >(Teacher); | ||||||
|  | 
 | ||||||
|  | /* Classes */ | ||||||
|  | export const getClassRepository = repositoryGetter<Class, ClassRepository>( | ||||||
|  |     Class | ||||||
|  | ); | ||||||
|  | export const getClassJoinRequestRepository = repositoryGetter< | ||||||
|  |     ClassJoinRequest, | ||||||
|  |     ClassJoinRequestRepository | ||||||
|  | >(ClassJoinRequest); | ||||||
|  | export const getTeacherInvitationRepository = repositoryGetter< | ||||||
|  |     TeacherInvitation, | ||||||
|  |     TeacherInvitationRepository | ||||||
|  | >(TeacherInvitationRepository); | ||||||
|  | 
 | ||||||
|  | /* Assignments */ | ||||||
|  | export const getAssignmentRepository = repositoryGetter< | ||||||
|  |     Assignment, | ||||||
|  |     AssignmentRepository | ||||||
|  | >(Assignment); | ||||||
|  | export const getGroupRepository = repositoryGetter<Group, GroupRepository>( | ||||||
|  |     Group | ||||||
|  | ); | ||||||
|  | export const getSubmissionRepository = repositoryGetter< | ||||||
|  |     Submission, | ||||||
|  |     SubmissionRepository | ||||||
|  | >(Submission); | ||||||
|  | 
 | ||||||
|  | /* Questions and answers */ | ||||||
|  | export const getQuestionRepository = repositoryGetter< | ||||||
|  |     Question, | ||||||
|  |     QuestionRepository | ||||||
|  | >(Question); | ||||||
|  | export const getAnswerRepository = repositoryGetter<Answer, AnswerRepository>( | ||||||
|  |     Answer | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | /* Learning content */ | ||||||
|  | export const getLearningObjectRepository = repositoryGetter< | ||||||
|  |     LearningObject, | ||||||
|  |     LearningObjectRepository | ||||||
|  | >(LearningObject); | ||||||
|  | export const getLearningPathRepository = repositoryGetter< | ||||||
|  |     LearningPath, | ||||||
|  |     LearningPathRepository | ||||||
|  | >(LearningPath); | ||||||
|  | export const getAttachmentRepository = repositoryGetter< | ||||||
|  |     Attachment, | ||||||
|  |     AttachmentRepository | ||||||
|  | >(Assignment); | ||||||
							
								
								
									
										11
									
								
								backend/src/data/users/student-repository.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								backend/src/data/users/student-repository.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
|  | import { Student } from '../../entities/users/student.entity.js'; | ||||||
|  | 
 | ||||||
|  | export class StudentRepository extends DwengoEntityRepository<Student> { | ||||||
|  |     public findByUsername(username: string): Promise<Student | null> { | ||||||
|  |         return this.findOne({ username: username }); | ||||||
|  |     } | ||||||
|  |     public deleteByUsername(username: string): Promise<void> { | ||||||
|  |         return this.deleteWhere({ username: username }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								backend/src/data/users/teacher-repository.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								backend/src/data/users/teacher-repository.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
|  | import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||||
|  | 
 | ||||||
|  | export class TeacherRepository extends DwengoEntityRepository<Teacher> { | ||||||
|  |     public findByUsername(username: string): Promise<Teacher | null> { | ||||||
|  |         return this.findOne({ username: username }); | ||||||
|  |     } | ||||||
|  |     public deleteByUsername(username: string): Promise<void> { | ||||||
|  |         return this.deleteWhere({ username: username }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								backend/src/data/users/user-repository.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								backend/src/data/users/user-repository.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
|  | import { User } from '../../entities/users/user.entity.js'; | ||||||
|  | 
 | ||||||
|  | export class UserRepository extends DwengoEntityRepository<User> { | ||||||
|  |     public findByUsername(username: string): Promise<User | null> { | ||||||
|  |         return this.findOne({ username: username }); | ||||||
|  |     } | ||||||
|  |     public deleteByUsername(username: string): Promise<void> { | ||||||
|  |         return this.deleteWhere({ username: username }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								backend/src/entities/assignments/assignment.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								backend/src/entities/assignments/assignment.entity.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | ||||||
|  | import { | ||||||
|  |     Entity, | ||||||
|  |     Enum, | ||||||
|  |     ManyToOne, | ||||||
|  |     OneToMany, | ||||||
|  |     PrimaryKey, | ||||||
|  |     Property, | ||||||
|  | } from '@mikro-orm/core'; | ||||||
|  | import { Class } from '../classes/class.entity.js'; | ||||||
|  | import { Group } from './group.entity.js'; | ||||||
|  | import { Language } from '../content/language.js'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | export class Assignment { | ||||||
|  |     @ManyToOne({ entity: () => Class, primary: true }) | ||||||
|  |     within!: Class; | ||||||
|  | 
 | ||||||
|  |     @PrimaryKey({ type: 'number' }) | ||||||
|  |     id!: number; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     title!: string; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'text' }) | ||||||
|  |     description!: string; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     learningPathHruid!: string; | ||||||
|  | 
 | ||||||
|  |     @Enum({ items: () => Language }) | ||||||
|  |     learningPathLanguage!: Language; | ||||||
|  | 
 | ||||||
|  |     @OneToMany({ entity: () => Group, mappedBy: 'assignment' }) | ||||||
|  |     groups!: Group[]; | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								backend/src/entities/assignments/group.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								backend/src/entities/assignments/group.entity.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | ||||||
|  | import { Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core'; | ||||||
|  | import { Assignment } from './assignment.entity.js'; | ||||||
|  | import { Student } from '../users/student.entity.js'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | export class Group { | ||||||
|  |     @ManyToOne({ entity: () => Assignment, primary: true }) | ||||||
|  |     assignment!: Assignment; | ||||||
|  | 
 | ||||||
|  |     @PrimaryKey({ type: 'integer' }) | ||||||
|  |     groupNumber!: number; | ||||||
|  | 
 | ||||||
|  |     @ManyToMany({ entity: () => Student }) | ||||||
|  |     members!: Student[]; | ||||||
|  | } | ||||||
							
								
								
									
										31
									
								
								backend/src/entities/assignments/submission.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								backend/src/entities/assignments/submission.entity.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | ||||||
|  | import { Student } from '../users/student.entity.js'; | ||||||
|  | import { Group } from './group.entity.js'; | ||||||
|  | import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; | ||||||
|  | import { Language } from '../content/language.js'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | export class Submission { | ||||||
|  |     @PrimaryKey({ type: 'string' }) | ||||||
|  |     learningObjectHruid!: string; | ||||||
|  | 
 | ||||||
|  |     @Enum({ items: () => Language, primary: true }) | ||||||
|  |     learningObjectLanguage!: Language; | ||||||
|  | 
 | ||||||
|  |     @PrimaryKey({ type: 'string' }) | ||||||
|  |     learningObjectVersion: string = '1'; | ||||||
|  | 
 | ||||||
|  |     @PrimaryKey({ type: 'integer' }) | ||||||
|  |     submissionNumber!: number; | ||||||
|  | 
 | ||||||
|  |     @ManyToOne({ entity: () => Student }) | ||||||
|  |     submitter!: Student; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'datetime' }) | ||||||
|  |     submissionTime!: Date; | ||||||
|  | 
 | ||||||
|  |     @ManyToOne({ entity: () => Group, nullable: true }) | ||||||
|  |     onBehalfOf?: Group; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'json' }) | ||||||
|  |     content!: string; | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								backend/src/entities/classes/class-join-request.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								backend/src/entities/classes/class-join-request.entity.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | ||||||
|  | import { Entity, Enum, ManyToOne } from '@mikro-orm/core'; | ||||||
|  | import { Student } from '../users/student.entity'; | ||||||
|  | import { Class } from './class.entity'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | export class ClassJoinRequest { | ||||||
|  |     @ManyToOne({ entity: () => Student, primary: true }) | ||||||
|  |     requester!: Student; | ||||||
|  | 
 | ||||||
|  |     @ManyToOne({ entity: () => Class, primary: true }) | ||||||
|  |     class!: Class; | ||||||
|  | 
 | ||||||
|  |     @Enum(() => ClassJoinRequestStatus) | ||||||
|  |     status!: ClassJoinRequestStatus; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export enum ClassJoinRequestStatus { | ||||||
|  |     Open = 'open', | ||||||
|  |     Accepted = 'accepted', | ||||||
|  |     Declined = 'declined', | ||||||
|  | } | ||||||
							
								
								
									
										25
									
								
								backend/src/entities/classes/class.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								backend/src/entities/classes/class.entity.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | ||||||
|  | import { | ||||||
|  |     Collection, | ||||||
|  |     Entity, | ||||||
|  |     ManyToMany, | ||||||
|  |     PrimaryKey, | ||||||
|  |     Property, | ||||||
|  | } from '@mikro-orm/core'; | ||||||
|  | import { v4 } from 'uuid'; | ||||||
|  | import { Teacher } from '../users/teacher.entity.js'; | ||||||
|  | import { Student } from '../users/student.entity.js'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | export class Class { | ||||||
|  |     @PrimaryKey() | ||||||
|  |     classId = v4(); | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     displayName!: string; | ||||||
|  | 
 | ||||||
|  |     @ManyToMany(() => Teacher) | ||||||
|  |     teachers!: Collection<Teacher>; | ||||||
|  | 
 | ||||||
|  |     @ManyToMany(() => Student) | ||||||
|  |     students!: Collection<Student>; | ||||||
|  | } | ||||||
							
								
								
									
										18
									
								
								backend/src/entities/classes/teacher-invitation.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								backend/src/entities/classes/teacher-invitation.entity.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | ||||||
|  | import { Entity, ManyToOne } from '@mikro-orm/core'; | ||||||
|  | import { Teacher } from '../users/teacher.entity.js'; | ||||||
|  | import { Class } from './class.entity.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Invitation of a teacher into a class (in order to teach it). | ||||||
|  |  */ | ||||||
|  | @Entity() | ||||||
|  | export class TeacherInvitation { | ||||||
|  |     @ManyToOne({ entity: () => Teacher, primary: true }) | ||||||
|  |     sender!: Teacher; | ||||||
|  | 
 | ||||||
|  |     @ManyToOne({ entity: () => Teacher, primary: true }) | ||||||
|  |     receiver!: Teacher; | ||||||
|  | 
 | ||||||
|  |     @ManyToOne({ entity: () => Class, primary: true }) | ||||||
|  |     class!: Class; | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								backend/src/entities/content/attachment.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								backend/src/entities/content/attachment.entity.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; | ||||||
|  | import { LearningObject } from './learning-object.entity.js'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | export class Attachment { | ||||||
|  |     @ManyToOne({ entity: () => LearningObject, primary: true }) | ||||||
|  |     learningObject!: LearningObject; | ||||||
|  | 
 | ||||||
|  |     @PrimaryKey({ type: 'integer' }) | ||||||
|  |     sequenceNumber!: number; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     mimeType!: string; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'blob' }) | ||||||
|  |     content!: Buffer; | ||||||
|  | } | ||||||
							
								
								
									
										6
									
								
								backend/src/entities/content/language.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								backend/src/entities/content/language.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | export enum Language { | ||||||
|  |     Dutch = 'nl', | ||||||
|  |     French = 'fr', | ||||||
|  |     English = 'en', | ||||||
|  |     Germany = 'de', | ||||||
|  | } | ||||||
|  | @ -0,0 +1,9 @@ | ||||||
|  | import { Language } from './language.js'; | ||||||
|  | 
 | ||||||
|  | export class LearningObjectIdentifier { | ||||||
|  |     constructor( | ||||||
|  |         public hruid: string, | ||||||
|  |         public language: Language, | ||||||
|  |         public version: string | ||||||
|  |     ) {} | ||||||
|  | } | ||||||
							
								
								
									
										106
									
								
								backend/src/entities/content/learning-object.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								backend/src/entities/content/learning-object.entity.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,106 @@ | ||||||
|  | import { | ||||||
|  |     Embeddable, | ||||||
|  |     Embedded, | ||||||
|  |     Entity, | ||||||
|  |     Enum, | ||||||
|  |     ManyToMany, | ||||||
|  |     OneToMany, | ||||||
|  |     PrimaryKey, | ||||||
|  |     Property, | ||||||
|  | } from '@mikro-orm/core'; | ||||||
|  | import { Language } from './language.js'; | ||||||
|  | import { Attachment } from './attachment.entity.js'; | ||||||
|  | import { Teacher } from '../users/teacher.entity.js'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | export class LearningObject { | ||||||
|  |     @PrimaryKey({ type: 'string' }) | ||||||
|  |     hruid!: string; | ||||||
|  | 
 | ||||||
|  |     @Enum({ items: () => Language, primary: true }) | ||||||
|  |     language!: Language; | ||||||
|  | 
 | ||||||
|  |     @PrimaryKey({ type: 'string' }) | ||||||
|  |     version: string = '1'; | ||||||
|  | 
 | ||||||
|  |     @ManyToMany({ entity: () => Teacher }) | ||||||
|  |     admins!: Teacher[]; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     title!: string; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'text' }) | ||||||
|  |     description!: string; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     contentType!: string; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'array' }) | ||||||
|  |     keywords: string[] = []; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'array', nullable: true }) | ||||||
|  |     targetAges?: number[]; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'bool' }) | ||||||
|  |     teacherExclusive: boolean = false; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'array' }) | ||||||
|  |     skosConcepts!: string[]; | ||||||
|  | 
 | ||||||
|  |     @Embedded({ entity: () => EducationalGoal, array: true }) | ||||||
|  |     educationalGoals: EducationalGoal[] = []; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     copyright: string = ''; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     license: string = ''; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'smallint', nullable: true }) | ||||||
|  |     difficulty?: number; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'integer' }) | ||||||
|  |     estimatedTime!: number; | ||||||
|  | 
 | ||||||
|  |     @Embedded({ entity: () => ReturnValue }) | ||||||
|  |     returnValue!: ReturnValue; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'bool' }) | ||||||
|  |     available: boolean = true; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'string', nullable: true }) | ||||||
|  |     contentLocation?: string; | ||||||
|  | 
 | ||||||
|  |     @OneToMany({ entity: () => Attachment, mappedBy: 'learningObject' }) | ||||||
|  |     attachments: Attachment[] = []; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'blob' }) | ||||||
|  |     content!: Buffer; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Embeddable() | ||||||
|  | export class EducationalGoal { | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     source!: string; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     id!: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Embeddable() | ||||||
|  | export class ReturnValue { | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     callbackUrl!: string; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'json' }) | ||||||
|  |     callbackSchema!: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export enum ContentType { | ||||||
|  |     Markdown = 'text/markdown', | ||||||
|  |     Image = 'image/image', | ||||||
|  |     Mpeg = 'audio/mpeg', | ||||||
|  |     Pdf = 'application/pdf', | ||||||
|  |     Extern = 'extern', | ||||||
|  |     Blockly = 'Blockly', | ||||||
|  | } | ||||||
							
								
								
									
										66
									
								
								backend/src/entities/content/learning-path.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								backend/src/entities/content/learning-path.entity.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,66 @@ | ||||||
|  | import { | ||||||
|  |     Embeddable, | ||||||
|  |     Embedded, | ||||||
|  |     Entity, | ||||||
|  |     Enum, | ||||||
|  |     ManyToMany, | ||||||
|  |     OneToOne, | ||||||
|  |     PrimaryKey, | ||||||
|  |     Property, | ||||||
|  | } from '@mikro-orm/core'; | ||||||
|  | import { Language } from './language.js'; | ||||||
|  | import { Teacher } from '../users/teacher.entity.js'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | export class LearningPath { | ||||||
|  |     @PrimaryKey({ type: 'string' }) | ||||||
|  |     hruid!: string; | ||||||
|  | 
 | ||||||
|  |     @Enum({ items: () => Language, primary: true }) | ||||||
|  |     language!: Language; | ||||||
|  | 
 | ||||||
|  |     @ManyToMany({ entity: () => Teacher }) | ||||||
|  |     admins!: Teacher[]; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     title!: string; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'text' }) | ||||||
|  |     description!: string; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'blob' }) | ||||||
|  |     image!: string; | ||||||
|  | 
 | ||||||
|  |     @Embedded({ entity: () => LearningPathNode, array: true }) | ||||||
|  |     nodes: LearningPathNode[] = []; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Embeddable() | ||||||
|  | export class LearningPathNode { | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     learningObjectHruid!: string; | ||||||
|  | 
 | ||||||
|  |     @Enum({ items: () => Language }) | ||||||
|  |     language!: Language; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     version!: string; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'longtext' }) | ||||||
|  |     instruction!: string; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'bool' }) | ||||||
|  |     startNode!: boolean; | ||||||
|  | 
 | ||||||
|  |     @Embedded({ entity: () => LearningPathTransition, array: true }) | ||||||
|  |     transitions!: LearningPathTransition[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Embeddable() | ||||||
|  | export class LearningPathTransition { | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     condition!: string; | ||||||
|  | 
 | ||||||
|  |     @OneToOne({ entity: () => LearningPathNode }) | ||||||
|  |     next!: LearningPathNode; | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								backend/src/entities/questions/answer.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								backend/src/entities/questions/answer.entity.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | ||||||
|  | import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; | ||||||
|  | import { Question } from './question.entity'; | ||||||
|  | import { Teacher } from '../users/teacher.entity'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | export class Answer { | ||||||
|  |     @ManyToOne({ entity: () => Teacher, primary: true }) | ||||||
|  |     author!: Teacher; | ||||||
|  | 
 | ||||||
|  |     @ManyToOne({ entity: () => Question, primary: true }) | ||||||
|  |     toQuestion!: Question; | ||||||
|  | 
 | ||||||
|  |     @PrimaryKey({ type: 'integer' }) | ||||||
|  |     sequenceNumber!: number; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'datetime' }) | ||||||
|  |     timestamp: Date = new Date(); | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'text' }) | ||||||
|  |     content!: string; | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								backend/src/entities/questions/question.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								backend/src/entities/questions/question.entity.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | ||||||
|  | import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; | ||||||
|  | import { Language } from '../content/language.js'; | ||||||
|  | import { Student } from '../users/student.entity.js'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | export class Question { | ||||||
|  |     @PrimaryKey({ type: 'string' }) | ||||||
|  |     learningObjectHruid!: string; | ||||||
|  | 
 | ||||||
|  |     @Enum({ items: () => Language, primary: true }) | ||||||
|  |     learningObjectLanguage!: Language; | ||||||
|  | 
 | ||||||
|  |     @PrimaryKey({ type: 'string' }) | ||||||
|  |     learningObjectVersion: string = '1'; | ||||||
|  | 
 | ||||||
|  |     @PrimaryKey({ type: 'integer' }) | ||||||
|  |     sequenceNumber!: number; | ||||||
|  | 
 | ||||||
|  |     @ManyToOne({ entity: () => Student }) | ||||||
|  |     author!: Student; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'datetime' }) | ||||||
|  |     timestamp: Date = new Date(); | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'text' }) | ||||||
|  |     content!: string; | ||||||
|  | } | ||||||
							
								
								
									
										22
									
								
								backend/src/entities/users/student.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								backend/src/entities/users/student.entity.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | ||||||
|  | import { User } from './user.entity.js'; | ||||||
|  | import { Collection, Entity, ManyToMany } from '@mikro-orm/core'; | ||||||
|  | import { Class } from '../classes/class.entity.js'; | ||||||
|  | import { Group } from '../assignments/group.entity.js'; | ||||||
|  | import { StudentRepository } from '../../data/users/student-repository.js'; | ||||||
|  | 
 | ||||||
|  | @Entity({ repository: () => StudentRepository }) | ||||||
|  | export class Student extends User { | ||||||
|  |     @ManyToMany(() => Class) | ||||||
|  |     classes!: Collection<Class>; | ||||||
|  | 
 | ||||||
|  |     @ManyToMany(() => Group) | ||||||
|  |     groups!: Collection<Group>; | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         public username: string, | ||||||
|  |         public firstName: string, | ||||||
|  |         public lastName: string | ||||||
|  |     ) { | ||||||
|  |         super(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								backend/src/entities/users/teacher.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								backend/src/entities/users/teacher.entity.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | ||||||
|  | import { Collection, Entity, ManyToMany } from '@mikro-orm/core'; | ||||||
|  | import { User } from './user.entity.js'; | ||||||
|  | import { Class } from '../classes/class.entity.js'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | export class Teacher extends User { | ||||||
|  |     @ManyToMany(() => Class) | ||||||
|  |     classes!: Collection<Class>; | ||||||
|  | } | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| import { Entity, PrimaryKey, Property } from '@mikro-orm/core'; | import { Entity, PrimaryKey, Property } from '@mikro-orm/core'; | ||||||
| 
 | 
 | ||||||
| @Entity() | @Entity({ abstract: true }) | ||||||
| export class User { | export abstract class User { | ||||||
|     @PrimaryKey({ type: 'number' }) |     @PrimaryKey({ type: 'string' }) | ||||||
|     id!: number; |     username!: string; | ||||||
| 
 | 
 | ||||||
|     @Property() |     @Property() | ||||||
|     firstName: string = ''; |     firstName: string = ''; | ||||||
|  | @ -1,12 +1,35 @@ | ||||||
| import { Options } from '@mikro-orm/core'; | import { Options } from '@mikro-orm/core'; | ||||||
| import { PostgreSqlDriver } from '@mikro-orm/postgresql'; | import { PostgreSqlDriver } from '@mikro-orm/postgresql'; | ||||||
|  | import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js'; | ||||||
|  | import { SqliteDriver } from '@mikro-orm/sqlite'; | ||||||
| 
 | 
 | ||||||
| const config: Options = { | const entities = ['dist/**/*.entity.js']; | ||||||
|     driver: PostgreSqlDriver, | const entitiesTs = ['src/**/*.entity.ts']; | ||||||
|     dbName: 'dwengo', | function config(testingMode: boolean = false): Options { | ||||||
|     entities: ['dist/**/*.entity.js'], |     if (testingMode) { | ||||||
|     entitiesTs: ['src/**/*.entity.ts'], |         return { | ||||||
|     debug: true, |             driver: SqliteDriver, | ||||||
| }; |             dbName: getEnvVar(EnvVars.DbName), | ||||||
|  |             entities: entities, | ||||||
|  |             entitiesTs: entitiesTs, | ||||||
|  | 
 | ||||||
|  |             // Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
 | ||||||
|  |             // (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint)
 | ||||||
|  |             dynamicImportProvider: (id) => import(id), | ||||||
|  |         }; | ||||||
|  |     } else { | ||||||
|  |         return { | ||||||
|  |             driver: PostgreSqlDriver, | ||||||
|  |             host: getEnvVar(EnvVars.DbHost), | ||||||
|  |             port: getNumericEnvVar(EnvVars.DbPort), | ||||||
|  |             dbName: getEnvVar(EnvVars.DbName), | ||||||
|  |             user: getEnvVar(EnvVars.DbUsername), | ||||||
|  |             password: getEnvVar(EnvVars.DbPassword), | ||||||
|  |             entities: entities, | ||||||
|  |             entitiesTs: entitiesTs, | ||||||
|  |             debug: true, | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| export default config; | export default config; | ||||||
|  |  | ||||||
|  | @ -1,6 +1,30 @@ | ||||||
| import { MikroORM } from '@mikro-orm/core'; | import { EntityManager, MikroORM } from '@mikro-orm/core'; | ||||||
| import config from './mikro-orm.config.js'; | import config from './mikro-orm.config.js'; | ||||||
|  | import { EnvVars, getEnvVar } from './util/envvars.js'; | ||||||
| 
 | 
 | ||||||
| export default async function initORM() { | let orm: MikroORM | undefined; | ||||||
|     await MikroORM.init(config); | export async function initORM(testingMode: boolean = false) { | ||||||
|  |     orm = await MikroORM.init(config(testingMode)); | ||||||
|  |     // Update the database scheme if necessary and enabled.
 | ||||||
|  |     if (getEnvVar(EnvVars.DbUpdate)) { | ||||||
|  |         await orm.schema.updateSchema(); | ||||||
|  |     } else { | ||||||
|  |         const diff = await orm.schema.getUpdateSchemaSQL(); | ||||||
|  |         if (diff) { | ||||||
|  |             throw Error( | ||||||
|  |                 'The database structure needs to be updated in order to fit the new database structure ' + | ||||||
|  |                     'of the app. In order to do so automatically, set the environment variable DWENGO_DB_UPDATE to true. ' + | ||||||
|  |                     'The following queries will then be executed:\n' + | ||||||
|  |                     diff | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | export function forkEntityManager(): EntityManager { | ||||||
|  |     if (!orm) { | ||||||
|  |         throw Error( | ||||||
|  |             'Accessing the Entity Manager before the ORM is fully initialized.' | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     return orm.em.fork(); | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										45
									
								
								backend/src/util/envvars.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								backend/src/util/envvars.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | ||||||
|  | const PREFIX = 'DWENGO_'; | ||||||
|  | const DB_PREFIX = PREFIX + 'DB_'; | ||||||
|  | 
 | ||||||
|  | type EnvVar = { key: string; required?: boolean; defaultValue?: any }; | ||||||
|  | 
 | ||||||
|  | export const EnvVars: { [key: string]: EnvVar } = { | ||||||
|  |     Port: { key: PREFIX + 'PORT', defaultValue: 3000 }, | ||||||
|  |     DbHost: { key: DB_PREFIX + 'HOST', required: true }, | ||||||
|  |     DbPort: { key: DB_PREFIX + 'PORT', defaultValue: 5432 }, | ||||||
|  |     DbName: { key: DB_PREFIX + 'NAME', defaultValue: 'dwengo' }, | ||||||
|  |     DbUsername: { key: DB_PREFIX + 'USERNAME', required: true }, | ||||||
|  |     DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true }, | ||||||
|  |     DbUpdate: { key: DB_PREFIX + 'UPDATE', defaultValue: false }, | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Returns the value of the given environment variable if it is set. | ||||||
|  |  * Otherwise, | ||||||
|  |  * - throw an error if the environment variable was required, | ||||||
|  |  * - return the default value if there is one and it was not required, | ||||||
|  |  * - return an empty string if the environment variable is not required and there is also no default. | ||||||
|  |  * @param envVar The properties of the environment variable (from the EnvVar object). | ||||||
|  |  */ | ||||||
|  | export function getEnvVar(envVar: EnvVar): string { | ||||||
|  |     const value: string | undefined = process.env[envVar.key]; | ||||||
|  |     if (value) { | ||||||
|  |         return value; | ||||||
|  |     } else if (envVar.required) { | ||||||
|  |         throw new Error(`Missing environment variable: ${envVar.key}`); | ||||||
|  |     } else { | ||||||
|  |         return envVar.defaultValue || ''; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getNumericEnvVar(envVar: EnvVar): number { | ||||||
|  |     const valueString = getEnvVar(envVar); | ||||||
|  |     const value = parseInt(valueString); | ||||||
|  |     if (isNaN(value)) { | ||||||
|  |         throw new Error( | ||||||
|  |             `Invalid value for environment variable ${envVar.key}: ${valueString}. Expected a number.` | ||||||
|  |         ); | ||||||
|  |     } else { | ||||||
|  |         return value; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										33
									
								
								backend/tests/data/users.test.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								backend/tests/data/users.test.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | ||||||
|  | import {setupTestApp} from "../setup-tests.js" | ||||||
|  | import {Student} from "../../src/entities/users/student.entity.js"; | ||||||
|  | import {describe, it, expect, beforeAll} from "vitest"; | ||||||
|  | import {StudentRepository} from "../../src/data/users/student-repository.js"; | ||||||
|  | import {getStudentRepository} from "../../src/data/repositories.js"; | ||||||
|  | 
 | ||||||
|  | const username = "teststudent"; | ||||||
|  | const firstName = "John"; | ||||||
|  | const lastName = "Doe"; | ||||||
|  | describe("StudentRepository", () => { | ||||||
|  |     let studentRepository: StudentRepository; | ||||||
|  | 
 | ||||||
|  |     beforeAll(async () => { | ||||||
|  |         await setupTestApp(); | ||||||
|  |         studentRepository = getStudentRepository(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it("should return the queried student after he was added", async () => { | ||||||
|  |         await studentRepository.insert(new Student(username, firstName, lastName)); | ||||||
|  | 
 | ||||||
|  |         let retrievedStudent = await studentRepository.findByUsername(username); | ||||||
|  |         expect(retrievedStudent).toBeTruthy(); | ||||||
|  |         expect(retrievedStudent?.firstName).toBe(firstName); | ||||||
|  |         expect(retrievedStudent?.lastName).toBe(lastName); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it("should no longer return the queried student after he was removed again", async () => { | ||||||
|  |         await studentRepository.deleteByUsername(username); | ||||||
|  | 
 | ||||||
|  |         let retrievedStudent = await studentRepository.findByUsername(username); | ||||||
|  |         expect(retrievedStudent).toBeNull(); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  | @ -4,6 +4,6 @@ describe("Sample test", () => { | ||||||
|     it("should sum to 2", () => { |     it("should sum to 2", () => { | ||||||
|         const expected = 2; |         const expected = 2; | ||||||
|         const result = 1 + 1; |         const result = 1 + 1; | ||||||
|         expect(result).toBe(expected); |         expect(result).equals(expected); | ||||||
|     }); |     }); | ||||||
| }) | }) | ||||||
|  |  | ||||||
							
								
								
									
										7
									
								
								backend/tests/setup-tests.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								backend/tests/setup-tests.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | import {initORM} from "../src/orm.js"; | ||||||
|  | import dotenv from "dotenv"; | ||||||
|  | 
 | ||||||
|  | export async function setupTestApp() { | ||||||
|  |     dotenv.config({path: ".env.test"}); | ||||||
|  |     await initORM(true); | ||||||
|  | } | ||||||
							
								
								
									
										8
									
								
								backend/vitest.config.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								backend/vitest.config.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | ||||||
|  | import { defineConfig } from 'vitest/config'; | ||||||
|  | 
 | ||||||
|  | export default defineConfig({ | ||||||
|  |     test: { | ||||||
|  |         environment: 'node', | ||||||
|  |         globals: true | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  | @ -6,11 +6,9 @@ services: | ||||||
|       POSTGRES_PASSWORD: postgres |       POSTGRES_PASSWORD: postgres | ||||||
|       POSTGRES_DB: postgres |       POSTGRES_DB: postgres | ||||||
|     ports: |     ports: | ||||||
|       - "5432:5432" |       - "5431:5432" | ||||||
|     network_mode: "host" |  | ||||||
|     volumes: |     volumes: | ||||||
|         - postgres_data:/var/lib/postgresql/data |         - postgres_data:/var/lib/postgresql/data | ||||||
|         - ./backend/config/db/init.sql:/docker-entrypoint-initdb.d/init.sql |  | ||||||
| 
 | 
 | ||||||
| volumes: | volumes: | ||||||
|     postgres_data: |     postgres_data: | ||||||
|  |  | ||||||
							
								
								
									
										1576
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1576
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger