fix(backend): Workaround voor autoincrement-problemen bij SQLite
SQLite (die we voor de automatische tests gebruiken) ondersteunt geen autoincrement op kolommen die deel uitmaken van een composite primary key. Hiervoor heb ik een workaround geïmplementeerd.
This commit is contained in:
parent
678ced55ba
commit
4dcd4671ca
9 changed files with 94 additions and 42 deletions
|
@ -9,10 +9,14 @@ export class AnswerRepository extends DwengoEntityRepository<Answer> {
|
|||
author: Teacher;
|
||||
content: string;
|
||||
}): Promise<Answer> {
|
||||
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<Answer[]> {
|
||||
|
|
|
@ -9,7 +9,14 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
|
|||
author: Student;
|
||||
content: string;
|
||||
}): Promise<Question> {
|
||||
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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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: () => {
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
41
backend/src/sqlite-autoincrement-workaround.ts
Normal file
41
backend/src/sqlite-autoincrement-workaround.ts
Normal file
|
@ -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<string, number> = 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<T extends object>(args: EventArgs<T>): 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<T>;
|
||||
if (property.primary && property.autoincrement && !(args.entity as Record<string, any>)[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<string, any>)[property.name] = nextSeqNumber + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue