diff --git a/backend/src/data/questions/answer-repository.ts b/backend/src/data/questions/answer-repository.ts index 6c45211c..6b58b4d0 100644 --- a/backend/src/data/questions/answer-repository.ts +++ b/backend/src/data/questions/answer-repository.ts @@ -9,10 +9,14 @@ export class AnswerRepository extends DwengoEntityRepository { author: Teacher; content: string; }): Promise { - const answerEntity = new Answer(); - answerEntity.toQuestion = answer.toQuestion; - answerEntity.author = answer.author; - answerEntity.content = answer.content; + const answerEntity = this.create( + { + toQuestion: answer.toQuestion, + author: answer.author, + content: answer.content, + timestamp: new Date() + } + ); return this.insert(answerEntity); } public findAllAnswersToQuestion(question: Question): Promise { diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 8852a9ba..a5d555da 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -9,7 +9,14 @@ export class QuestionRepository extends DwengoEntityRepository { author: Student; content: string; }): Promise { - const questionEntity = new Question(); + const questionEntity = this.create({ + learningObjectHruid: question.loId.hruid, + learningObjectLanguage: question.loId.language, + learningObjectVersion: question.loId.version, + author: question.author, + content: question.content, + timestamp: new Date() + }); questionEntity.learningObjectHruid = question.loId.hruid; questionEntity.learningObjectLanguage = question.loId.language; questionEntity.learningObjectVersion = question.loId.version; diff --git a/backend/src/entities/questions/answer.entity.ts b/backend/src/entities/questions/answer.entity.ts index b73c7014..14021689 100644 --- a/backend/src/entities/questions/answer.entity.ts +++ b/backend/src/entities/questions/answer.entity.ts @@ -25,8 +25,8 @@ export class Answer { }) toQuestion!: Question; - @PrimaryKey({ type: 'integer' }) - sequenceNumber!: number; + @PrimaryKey({ type: 'integer', autoincrement: true }) + sequenceNumber?: number; @Property({ type: 'datetime' }) timestamp: Date = new Date(); diff --git a/backend/src/entities/questions/question.entity.ts b/backend/src/entities/questions/question.entity.ts index 689b6ca1..14188ea9 100644 --- a/backend/src/entities/questions/question.entity.ts +++ b/backend/src/entities/questions/question.entity.ts @@ -23,8 +23,8 @@ export class Question { @PrimaryKey({ type: 'string' }) learningObjectVersion: string = '1'; - @PrimaryKey({ type: 'integer' }) - sequenceNumber!: number; + @PrimaryKey({ type: 'integer', autoincrement: true }) + sequenceNumber?: number; @ManyToOne({ entity: () => { diff --git a/backend/src/mikro-orm.config.ts b/backend/src/mikro-orm.config.ts index f9629bef..283be0c3 100644 --- a/backend/src/mikro-orm.config.ts +++ b/backend/src/mikro-orm.config.ts @@ -24,6 +24,7 @@ import { LearningPath } from './entities/content/learning-path.entity.js'; import { Answer } from './entities/questions/answer.entity.js'; import { Question } from './entities/questions/question.entity.js'; +import {SqliteAutoincrementSubscriber} from "./sqlite-autoincrement-workaround"; const entities = [ User, @@ -47,6 +48,7 @@ function config(testingMode: boolean = false): Options { return { driver: SqliteDriver, dbName: getEnvVar(EnvVars.DbName), + subscribers: [new SqliteAutoincrementSubscriber()], entities: entities, // EntitiesTs: entitiesTs, diff --git a/backend/src/orm.ts b/backend/src/orm.ts index 88decd92..7776df61 100644 --- a/backend/src/orm.ts +++ b/backend/src/orm.ts @@ -1,4 +1,4 @@ -import { EntityManager, MikroORM } from '@mikro-orm/core'; +import {EntityManager, MikroORM} from '@mikro-orm/core'; import config from './mikro-orm.config.js'; import { EnvVars, getEnvVar } from './util/envvars.js'; import { getLogger, Logger } from './logging/initalize.js'; diff --git a/backend/src/sqlite-autoincrement-workaround.ts b/backend/src/sqlite-autoincrement-workaround.ts new file mode 100644 index 00000000..62c10611 --- /dev/null +++ b/backend/src/sqlite-autoincrement-workaround.ts @@ -0,0 +1,41 @@ +import {EntityProperty, EventArgs, EventSubscriber} from "@mikro-orm/core"; + +/** + * The tests are ran on an in-memory SQLite database. However, SQLite does not allow fields which are part of composite + * primary keys to be autoincremented (while PostgreSQL, which we use in production, does). This Subscriber works around + * the issue by remembering the highest values for every autoincremented part of a primary key and assigning them when + * creating a new entity. + * + * However, it is important to note the following limitations: + * - this class can only be used for in-memory SQLite databases since the information on what the highest sequence + * number for each of the properties is, is only saved transiently. + * - automatically setting the generated "autoincremented" value for properties only works when the entity is created + * via an entityManager.create(...) or repo.create(...) method. Otherwise, onInit will not be called and therefore, + * the sequence number will not be filled in. + */ +export class SqliteAutoincrementSubscriber implements EventSubscriber { + private sequenceNumbersForEntityType: Map = new Map(); + + /** + * When an entity with an autoincremented property which is part of the composite private key is created, + * automatically fill this property so we won't face not-null-constraint exceptions when persisting it. + */ + onInit(args: EventArgs): void { + if (!args.meta.compositePK) { + return; // If there is not a composite primary key, autoincrement works fine with SQLite anyway. + } + + for (let prop of Object.values(args.meta.properties)) { + const property = prop as EntityProperty; + if (property.primary && property.autoincrement && !(args.entity as Record)[property.name]) { + // Obtain and increment sequence number of this entity. + const propertyKey = args.meta.class.name + "." + property.name; + const nextSeqNumber = this.sequenceNumbersForEntityType.get(propertyKey) || 0; + this.sequenceNumbersForEntityType.set(propertyKey, nextSeqNumber + 1); + + // Set the property accordingly. + (args.entity as Record)[property.name] = nextSeqNumber + 1; + } + } + } +} diff --git a/backend/tests/data/questions/answers.test.ts b/backend/tests/data/questions/answers.test.ts index 6ee63d2e..4b15c6b0 100644 --- a/backend/tests/data/questions/answers.test.ts +++ b/backend/tests/data/questions/answers.test.ts @@ -9,7 +9,6 @@ import { import { QuestionRepository } from '../../../src/data/questions/question-repository'; import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier'; import { Language } from '../../../src/entities/content/language'; -import { Question } from '../../../src/entities/questions/question.entity'; import { TeacherRepository } from '../../../src/data/users/teacher-repository'; describe('AnswerRepository', () => { @@ -40,27 +39,27 @@ describe('AnswerRepository', () => { expect(answers[1].content).toBeOneOf(['answer', 'answer2']); }); - // it('should create an answer to a question', async () => { - // const teacher = await teacherRepository.findByUsername('FooFighters'); - // const id = new LearningObjectIdentifier('id05', Language.English, '1'); - // const questions = - // await questionRepository.findAllQuestionsAboutLearningObject(id); + it('should create an answer to a question', async () => { + const teacher = await teacherRepository.findByUsername('FooFighters'); + const id = new LearningObjectIdentifier('id05', Language.English, '1'); + const questions = + await questionRepository.findAllQuestionsAboutLearningObject(id); - // const question = questions[0]; + const question = questions[0]; - // await answerRepository.createAnswer({ - // toQuestion: question, - // author: teacher!, - // content: 'created answer', - // }); + await answerRepository.createAnswer({ + toQuestion: question, + author: teacher!, + content: 'created answer', + }); - // const answers = - // await answerRepository.findAllAnswersToQuestion(question); + const answers = + await answerRepository.findAllAnswersToQuestion(question); - // expect(answers).toBeTruthy(); - // expect(answers).toHaveLength(1); - // expect(answers[0].content).toBe('created answer'); - // }); + expect(answers).toBeTruthy(); + expect(answers).toHaveLength(1); + expect(answers[0].content).toBe('created answer'); + }); it('should not find a removed answer', async () => { const id = new LearningObjectIdentifier('id04', Language.English, '1'); diff --git a/backend/tests/data/questions/questions.test.ts b/backend/tests/data/questions/questions.test.ts index ad47e5b3..dc0d6285 100644 --- a/backend/tests/data/questions/questions.test.ts +++ b/backend/tests/data/questions/questions.test.ts @@ -10,7 +10,6 @@ import { StudentRepository } from '../../../src/data/users/student-repository'; import { LearningObjectRepository } from '../../../src/data/content/learning-object-repository'; import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier'; import { Language } from '../../../src/entities/content/language'; -import { Question } from '../../../src/entities/questions/question.entity'; describe('QuestionRepository', () => { let questionRepository: QuestionRepository; @@ -33,20 +32,20 @@ describe('QuestionRepository', () => { expect(questions).toHaveLength(2); }); - // it('should create new question', async () => { - // const id = new LearningObjectIdentifier('id03', Language.English, '1'); - // const student = await studentRepository.findByUsername('Noordkaap'); - // await questionRepository.createQuestion({ - // loId: id, - // author: student!, - // content: 'question?', - // }); - // const question = - // await questionRepository.findAllQuestionsAboutLearningObject(id); + it('should create new question', async () => { + const id = new LearningObjectIdentifier('id03', Language.English, '1'); + const student = await studentRepository.findByUsername('Noordkaap'); + await questionRepository.createQuestion({ + loId: id, + author: student!, + content: 'question?', + }); + const question = + await questionRepository.findAllQuestionsAboutLearningObject(id); - // expect(question).toBeTruthy(); - // expect(question).toHaveLength(1); - // }); + expect(question).toBeTruthy(); + expect(question).toHaveLength(1); + }); it('should not find removed question', async () => { const id = new LearningObjectIdentifier('id04', Language.English, '1');