Skip to content

Commit e200f7f

Browse files
authored
Merge pull request #371 from openscript-ch/337-the-same-child-cannot-be-assigned-to-different-studies
337 the same child cannot be assigned to different studies
2 parents 19c1bf6 + 438634c commit e200f7f

30 files changed

+602
-138
lines changed

.changeset/vast-showers-tan.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@quassel/frontend": patch
3+
"@quassel/backend": patch
4+
"@quassel/ui": patch
5+
---
6+
7+
Redesign data model. Allow assigning Participants to multiple studies.

apps/backend/db/migrations/.snapshot-postgres.json

Lines changed: 125 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -234,55 +234,6 @@
234234
}
235235
}
236236
},
237-
{
238-
"columns": {
239-
"id": {
240-
"name": "id",
241-
"type": "serial",
242-
"unsigned": false,
243-
"autoincrement": true,
244-
"primary": true,
245-
"nullable": false,
246-
"mappedType": "integer"
247-
},
248-
"title": {
249-
"name": "title",
250-
"type": "varchar(255)",
251-
"unsigned": false,
252-
"autoincrement": false,
253-
"primary": false,
254-
"nullable": false,
255-
"length": 255,
256-
"mappedType": "string"
257-
}
258-
},
259-
"name": "study",
260-
"schema": "public",
261-
"indexes": [
262-
{
263-
"keyName": "study_pkey",
264-
"columnNames": [
265-
"id"
266-
],
267-
"composite": false,
268-
"constraint": true,
269-
"primary": true,
270-
"unique": true
271-
}
272-
],
273-
"checks": [],
274-
"foreignKeys": {},
275-
"nativeEnums": {
276-
"UserRole": {
277-
"name": "UserRole",
278-
"schema": "public",
279-
"items": [
280-
"ASSISTANT",
281-
"ADMIN"
282-
]
283-
}
284-
}
285-
},
286237
{
287238
"columns": {
288239
"id": {
@@ -354,15 +305,6 @@
354305
"length": 6,
355306
"mappedType": "datetime"
356307
},
357-
"study_id": {
358-
"name": "study_id",
359-
"type": "int",
360-
"unsigned": false,
361-
"autoincrement": false,
362-
"primary": false,
363-
"nullable": false,
364-
"mappedType": "integer"
365-
},
366308
"participant_id": {
367309
"name": "participant_id",
368310
"type": "bigint",
@@ -389,18 +331,6 @@
389331
],
390332
"checks": [],
391333
"foreignKeys": {
392-
"questionnaire_study_id_foreign": {
393-
"constraintName": "questionnaire_study_id_foreign",
394-
"columnNames": [
395-
"study_id"
396-
],
397-
"localTableName": "public.questionnaire",
398-
"referencedColumnNames": [
399-
"id"
400-
],
401-
"referencedTableName": "public.study",
402-
"updateRule": "cascade"
403-
},
404334
"questionnaire_participant_id_foreign": {
405335
"constraintName": "questionnaire_participant_id_foreign",
406336
"columnNames": [
@@ -659,6 +589,131 @@
659589
}
660590
}
661591
},
592+
{
593+
"columns": {
594+
"id": {
595+
"name": "id",
596+
"type": "serial",
597+
"unsigned": false,
598+
"autoincrement": true,
599+
"primary": true,
600+
"nullable": false,
601+
"mappedType": "integer"
602+
},
603+
"title": {
604+
"name": "title",
605+
"type": "varchar(255)",
606+
"unsigned": false,
607+
"autoincrement": false,
608+
"primary": false,
609+
"nullable": false,
610+
"length": 255,
611+
"mappedType": "string"
612+
}
613+
},
614+
"name": "study",
615+
"schema": "public",
616+
"indexes": [
617+
{
618+
"keyName": "study_pkey",
619+
"columnNames": [
620+
"id"
621+
],
622+
"composite": false,
623+
"constraint": true,
624+
"primary": true,
625+
"unique": true
626+
}
627+
],
628+
"checks": [],
629+
"foreignKeys": {},
630+
"nativeEnums": {
631+
"UserRole": {
632+
"name": "UserRole",
633+
"schema": "public",
634+
"items": [
635+
"ASSISTANT",
636+
"ADMIN"
637+
]
638+
}
639+
}
640+
},
641+
{
642+
"columns": {
643+
"study_id": {
644+
"name": "study_id",
645+
"type": "int",
646+
"unsigned": false,
647+
"autoincrement": false,
648+
"primary": false,
649+
"nullable": false,
650+
"mappedType": "integer"
651+
},
652+
"participant_id": {
653+
"name": "participant_id",
654+
"type": "bigint",
655+
"unsigned": false,
656+
"autoincrement": false,
657+
"primary": false,
658+
"nullable": false,
659+
"mappedType": "bigint"
660+
}
661+
},
662+
"name": "study_participants",
663+
"schema": "public",
664+
"indexes": [
665+
{
666+
"keyName": "study_participants_pkey",
667+
"columnNames": [
668+
"study_id",
669+
"participant_id"
670+
],
671+
"composite": true,
672+
"constraint": true,
673+
"primary": true,
674+
"unique": true
675+
}
676+
],
677+
"checks": [],
678+
"foreignKeys": {
679+
"study_participants_study_id_foreign": {
680+
"constraintName": "study_participants_study_id_foreign",
681+
"columnNames": [
682+
"study_id"
683+
],
684+
"localTableName": "public.study_participants",
685+
"referencedColumnNames": [
686+
"id"
687+
],
688+
"referencedTableName": "public.study",
689+
"deleteRule": "cascade",
690+
"updateRule": "cascade"
691+
},
692+
"study_participants_participant_id_foreign": {
693+
"constraintName": "study_participants_participant_id_foreign",
694+
"columnNames": [
695+
"participant_id"
696+
],
697+
"localTableName": "public.study_participants",
698+
"referencedColumnNames": [
699+
"id"
700+
],
701+
"referencedTableName": "public.participant",
702+
"deleteRule": "cascade",
703+
"updateRule": "cascade"
704+
}
705+
},
706+
"nativeEnums": {
707+
"UserRole": {
708+
"name": "UserRole",
709+
"schema": "public",
710+
"items": [
711+
"ASSISTANT",
712+
"ADMIN"
713+
]
714+
}
715+
}
716+
},
662717
{
663718
"columns": {
664719
"id": {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Migration } from '@mikro-orm/migrations';
2+
import { Questionnaire } from '../../src/research/questionnaires/questionnaire.entity';
3+
4+
export class Migration20250305095459 extends Migration {
5+
6+
override async up(): Promise<void> {
7+
const em = this.getEntityManager();
8+
const qb = em.createQueryBuilder(Questionnaire)
9+
10+
this.addSql(`create table "study_participants" ("study_id" int not null, "participant_id" bigint not null, constraint "study_participants_pkey" primary key ("study_id", "participant_id"));`);
11+
12+
this.addSql(`alter table "study_participants" add constraint "study_participants_study_id_foreign" foreign key ("study_id") references "study" ("id") on update cascade on delete cascade;`);
13+
this.addSql(`alter table "study_participants" add constraint "study_participants_participant_id_foreign" foreign key ("participant_id") references "participant" ("id") on update cascade on delete cascade;`);
14+
15+
const questionnaires = await qb.select(["participant_id","study_id"]).distinct().execute<{ participant: number, study_id: number }[]>()
16+
17+
for (const { participant, study_id } of questionnaires) {
18+
this.addSql(`insert into "study_participants" ("study_id", "participant_id") values (${study_id}, ${participant});`);
19+
}
20+
21+
this.addSql(`alter table "questionnaire" drop constraint "questionnaire_study_id_foreign";`);
22+
23+
this.addSql(`alter table "questionnaire" drop column "study_id";`);
24+
}
25+
26+
override async down(): Promise<void> {
27+
this.addSql(`drop table if exists "study_participants" cascade;`);
28+
29+
this.addSql(`alter table "questionnaire" add column "study_id" int not null;`);
30+
this.addSql(`alter table "questionnaire" add constraint "questionnaire_study_id_foreign" foreign key ("study_id") references "study" ("id") on update cascade;`);
31+
}
32+
33+
}

apps/backend/src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ async function bootstrap() {
1313
app.enableCors({
1414
credentials: true,
1515
origin: configService.get("cors.origin"),
16+
methods: ["GET", "POST", "PUT", "DELETE"],
1617
exposedHeaders: ["Content-Disposition"],
1718
});
1819
app.enableShutdownHooks();

apps/backend/src/research/participants/participant.entity.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { BaseEntity, Collection, Entity, OneToMany, PrimaryKey, Property } from "@mikro-orm/core";
1+
import { BaseEntity, Collection, Entity, ManyToMany, OneToMany, PrimaryKey, Property } from "@mikro-orm/core";
22
import { Carer } from "../../defaults/carers/carer.entity";
33
import { Language } from "../../defaults/languages/language.entity";
44
import { Questionnaire } from "../questionnaires/questionnaire.entity";
5+
import { Study } from "../studies/study.entity";
56

67
@Entity()
78
export class Participant extends BaseEntity {
@@ -19,4 +20,7 @@ export class Participant extends BaseEntity {
1920

2021
@OneToMany(() => Language, (language) => language.participant)
2122
languages = new Collection<Language>(this);
23+
24+
@ManyToMany(() => Study, (study) => study.participants)
25+
studies = new Collection<Study>(this);
2226
}

apps/backend/src/research/questionnaires/questionnaire.dto.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { ApiProperty, PartialType } from "@nestjs/swagger";
22
import { Expose, Type } from "class-transformer";
33
import { IsDateString, IsNotEmpty, IsOptional } from "class-validator";
4-
import { StudyResponseDto } from "../studies/study.dto";
54
import { EntryResponseDto } from "../entries/entry.dto";
65
import { ParticipantResponseDto } from "../participants/participant.dto";
76

@@ -41,10 +40,6 @@ export class QuestionnaireResponseDto extends QuestionnaireBaseDto {
4140
@Expose()
4241
createdAt: Date;
4342

44-
@Type(() => StudyResponseDto)
45-
@Expose()
46-
study: StudyResponseDto;
47-
4843
@Type(() => ParticipantResponseDto)
4944
@Expose()
5045
participant: ParticipantResponseDto;
@@ -57,7 +52,6 @@ export class QuestionnaireDetailResponseDto extends QuestionnaireResponseDto {
5752
}
5853

5954
export class QuestionnaireCreationDto extends QuestionnaireBaseDto {
60-
study: number;
6155
participant: number;
6256
}
6357
export class QuestionnaireMutationDto extends PartialType(QuestionnaireCreationDto) {}

apps/backend/src/research/questionnaires/questionnaire.entity.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Collection, Entity, Formula, ManyToOne, OneToMany, Property } from "@mikro-orm/core";
22
import { BaseEntity } from "../../common/entities/base.entity";
3-
import { Study } from "../studies/study.entity";
43
import { Participant } from "../participants/participant.entity";
54
import { Entry } from "../entries/entry.entity";
65

@@ -24,9 +23,6 @@ export class Questionnaire extends BaseEntity {
2423
@Property()
2524
completedAt?: Date;
2625

27-
@ManyToOne()
28-
study: Study;
29-
3026
@ManyToOne()
3127
participant: Participant;
3228

apps/backend/src/research/questionnaires/questionnaires.service.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export class QuestionnairesService {
5252
throw e;
5353
}
5454

55-
return (await questionnaire.populate(["entries", "entries.carer", "entries.entryLanguages.language", "participant", "study"])).toObject();
55+
return (await questionnaire.populate(["entries", "entries.carer", "entries.entryLanguages.language", "participant"])).toObject();
5656
}
5757

5858
async findAll({
@@ -69,7 +69,7 @@ export class QuestionnairesService {
6969
return (
7070
await this.questionnaireRepository.findAll({
7171
where: { ...(participantId && { participant: participantId }), ...(studyTitle && { study: { title: { $fulltext: studyTitle } } }) },
72-
populate: ["study", "participant"],
72+
populate: ["participant"],
7373
orderBy: sortBy && { [sortBy]: sortOrder },
7474
})
7575
).map((questionnaire) => questionnaire.toObject());
@@ -78,7 +78,7 @@ export class QuestionnairesService {
7878
async findOne(id: number) {
7979
return (
8080
await this.questionnaireRepository.findOneOrFail(id, {
81-
populate: ["entries", "entries.carer", "entries.entryLanguages.language", "participant", "study"],
81+
populate: ["entries", "entries.carer", "entries.entryLanguages.language", "participant"],
8282
})
8383
).toObject();
8484
}
@@ -93,7 +93,7 @@ export class QuestionnairesService {
9393

9494
async update(id: number, questionnaireMutationDto: QuestionnaireMutationDto) {
9595
const questionnaire = await this.questionnaireRepository.findOneOrFail(id, {
96-
populate: ["entries", "entries.carer", "entries.entryLanguages.language", "participant", "study"],
96+
populate: ["entries", "entries.carer", "entries.entryLanguages.language", "participant"],
9797
});
9898

9999
const prevQuestionnaire = await this.questionnaireRepository.findOne(

apps/backend/src/research/research.module.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,19 @@ import { Entry } from "./entries/entry.entity";
1515
import { EntryLanguage } from "./entry-languages/entry-language.entity";
1616
import { Questionnaire } from "./questionnaires/questionnaire.entity";
1717
import { Study } from "./studies/study.entity";
18+
import { StudyParticipantsController } from "./study-participants/study-participants.controller";
19+
import { StudyParticipantsService } from "./study-participants/study-participants.service";
1820

1921
@Module({
2022
imports: [MikroOrmModule.forFeature([Entry, EntryLanguage, Participant, Questionnaire, Study])],
21-
providers: [ParticipantsService, EntriesService, EntryLanguagesService, QuestionnairesService, StudiesService],
22-
controllers: [ParticipantsController, EntriesController, QuestionnairesController, EntryLanguagesController, StudiesController],
23+
providers: [ParticipantsService, EntriesService, EntryLanguagesService, QuestionnairesService, StudiesService, StudyParticipantsService],
24+
controllers: [
25+
ParticipantsController,
26+
EntriesController,
27+
QuestionnairesController,
28+
EntryLanguagesController,
29+
StudiesController,
30+
StudyParticipantsController,
31+
],
2332
})
2433
export class ResearchModule {}

0 commit comments

Comments
 (0)