diff --git a/CHANGELOG.md b/CHANGELOG.md
index 619f6055..67fee84b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,7 @@ Another example [here](https://co-pilot.dev/changelog)
- Add new endpoint to revoke refresh token ([#116](https://github.com/chingu-x/chingu-dashboard-be/pull/116))
- Add meetingId to sprints/teams endpoint (([#119](https://github.com/chingu-x/chingu-dashboard-be/pull/119)))
- Add new endpoint to select tech stack items ([#125](https://github.com/chingu-x/chingu-dashboard-be/pull/125))
+- Add check in form response table, seed data, POST endpoint for submitting check in form ([#126](https://github.com/chingu-x/chingu-dashboard-be/pull/126))
- Add multiple device support ([#128](https://github.com/chingu-x/chingu-dashboard-be/pull/128))
### Changed
diff --git a/README.md b/README.md
index 64342750..733f17a6 100644
--- a/README.md
+++ b/README.md
@@ -176,6 +176,11 @@ $ yarn docker:down
$ yarn docker:clean
```
+## Custom Pipes
+### FormInputValidationPipe
+For use with form responses, this pipe validates that the responses or response (array) values include a questionId and at least one input value of any type.
+
+Example: `@Body(new FormInputValidationPipe())`
## Custom Decorators
diff --git a/package.json b/package.json
index cfb146f8..a1dba961 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
"@nestjs/platform-express": "^10.0.0",
"@nestjs/schedule": "^4.0.0",
"@nestjs/swagger": "^7.1.11",
- "@prisma/client": "^5.3.1",
+ "@prisma/client": "^5.10.2",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
diff --git a/prisma/migrations/20240401064842_add_checkin_form_tables_and_fields/migration.sql b/prisma/migrations/20240401064842_add_checkin_form_tables_and_fields/migration.sql
new file mode 100644
index 00000000..481b9f89
--- /dev/null
+++ b/prisma/migrations/20240401064842_add_checkin_form_tables_and_fields/migration.sql
@@ -0,0 +1,31 @@
+-- DropIndex
+DROP INDEX "Response_questionId_responseGroupId_key";
+
+-- CreateTable
+CREATE TABLE "FormResponseCheckin" (
+ "id" SERIAL NOT NULL,
+ "voyageTeamMemberId" INTEGER NOT NULL,
+ "sprintId" INTEGER NOT NULL,
+ "adminComments" TEXT,
+ "feedbackSent" BOOLEAN NOT NULL DEFAULT false,
+ "responseGroupId" INTEGER NOT NULL,
+ "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "FormResponseCheckin_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "FormResponseCheckin_responseGroupId_key" ON "FormResponseCheckin"("responseGroupId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "FormResponseCheckin_voyageTeamMemberId_sprintId_key" ON "FormResponseCheckin"("voyageTeamMemberId", "sprintId");
+
+-- AddForeignKey
+ALTER TABLE "FormResponseCheckin" ADD CONSTRAINT "FormResponseCheckin_voyageTeamMemberId_fkey" FOREIGN KEY ("voyageTeamMemberId") REFERENCES "VoyageTeamMember"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "FormResponseCheckin" ADD CONSTRAINT "FormResponseCheckin_sprintId_fkey" FOREIGN KEY ("sprintId") REFERENCES "Sprint"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "FormResponseCheckin" ADD CONSTRAINT "FormResponseCheckin_responseGroupId_fkey" FOREIGN KEY ("responseGroupId") REFERENCES "ResponseGroup"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 2088d8ea..c066f2fd 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -139,6 +139,7 @@ model Sprint {
updatedAt DateTime @updatedAt
teamMeetings TeamMeeting[]
+ checkinForms FormResponseCheckin[]
@@unique(fields: [voyageId, number], name: "voyageSprintNumber")
}
@@ -221,6 +222,7 @@ model VoyageTeamMember {
projectIdeaVotes ProjectIdeaVote[]
teamResources TeamResource[]
projectFeatures ProjectFeature[]
+ checkinForms FormResponseCheckin[]
@@unique(fields: [userId, voyageTeamId], name: "userVoyageId")
}
@@ -532,8 +534,6 @@ model Response {
createdAt DateTime @default(now()) @db.Timestamptz()
updatedAt DateTime @updatedAt
-
- @@unique(fields: [questionId, responseGroupId], name: "questionResponseGroup")
}
model ResponseGroup {
@@ -546,20 +546,37 @@ model ResponseGroup {
soloProject SoloProject?
voyageApplication VoyageApplication?
formResponseMeeting FormResponseMeeting?
+ formResponseCheckin FormResponseCheckin?
}
model FormResponseMeeting {
- id Int @id @default(autoincrement())
- formId Int
- form Form @relation(fields: [formId], references: [id])
- meetingId Int
- meeting TeamMeeting @relation(fields: [meetingId], references: [id], onDelete: Cascade)
+ id Int @id @default(autoincrement())
+ formId Int
+ form Form @relation(fields: [formId], references: [id])
+ meetingId Int
+ meeting TeamMeeting @relation(fields: [meetingId], references: [id], onDelete: Cascade)
+ responseGroupId Int? @unique
+ responseGroup ResponseGroup? @relation(fields: [responseGroupId], references: [id], onDelete: Restrict)
createdAt DateTime @default(now()) @db.Timestamptz()
updatedAt DateTime @updatedAt
- responseGroupId Int? @unique
- responseGroup ResponseGroup? @relation(fields: [responseGroupId], references: [id], onDelete: Restrict)
-
@@unique(fields: [formId, meetingId], name: "meetingFormId")
}
+
+model FormResponseCheckin {
+ id Int @id @default(autoincrement())
+ voyageTeamMemberId Int
+ voyageTeamMember VoyageTeamMember @relation(fields: [voyageTeamMemberId], references: [id], onDelete: Cascade)
+ sprintId Int
+ sprint Sprint @relation(fields: [sprintId], references: [id], onDelete: Cascade)
+ adminComments String?
+ feedbackSent Boolean @default(false)
+ responseGroupId Int @unique
+ responseGroup ResponseGroup @relation(fields: [responseGroupId], references: [id], onDelete: Restrict)
+
+ createdAt DateTime @default(now()) @db.Timestamptz()
+ updatedAt DateTime @updatedAt
+
+ @@unique(fields: [voyageTeamMemberId, sprintId], name: "voyageTeamMemberSprintId")
+}
diff --git a/prisma/seed/checkinform-responses.ts b/prisma/seed/checkinform-responses.ts
new file mode 100644
index 00000000..f5abd5f7
--- /dev/null
+++ b/prisma/seed/checkinform-responses.ts
@@ -0,0 +1,234 @@
+import { formSelect } from "../../src/forms/forms.service";
+import { prisma } from "./prisma-client";
+
+// TODO: move these to a helper function file
+const getRandomOptionId = async (
+ optionGroupId: number,
+ numberOfChoices: number,
+) => {
+ const choicesArray = [];
+ const choices = await prisma.optionChoice.findMany({
+ where: {
+ optionGroupId,
+ },
+ });
+ while (choicesArray.length < numberOfChoices) {
+ const choice = choices[Math.floor(Math.random() * choices.length)].id;
+ if (!choicesArray.includes(choice)) {
+ choicesArray.push(choice);
+ }
+ }
+ return choicesArray;
+};
+
+const getTeamMembers = async (teamMemberId: number) => {
+ const team = await prisma.voyageTeam.findFirst({
+ where: {
+ voyageTeamMembers: {
+ some: {
+ id: teamMemberId,
+ },
+ },
+ },
+ select: {
+ voyageTeamMembers: {
+ select: {
+ member: {
+ select: {
+ discordId: true,
+ },
+ },
+ },
+ },
+ },
+ });
+ return team.voyageTeamMembers.map((m) => m.member.discordId);
+};
+
+const populateQuestionResponses = async (
+ question: any,
+ teamMemberId: number,
+ responseGroupId: number,
+) => {
+ const data: any = {
+ questionId: question.id,
+ responseGroupId: responseGroupId,
+ };
+ switch (question.inputType.name) {
+ case "text": {
+ await prisma.response.create({
+ data: {
+ ...data,
+ text: `Text response for Question id ${question.id}.`,
+ },
+ });
+ break;
+ }
+ case "radio": {
+ const radioChoices = await getRandomOptionId(
+ question.optionGroupId,
+ 1,
+ );
+ await prisma.response.create({
+ data: {
+ ...data,
+ optionChoiceId: radioChoices[0],
+ },
+ });
+ break;
+ }
+ case "radioGroup": {
+ // get all subquestions
+ const subQuestions = await prisma.question.findMany({
+ where: {
+ parentQuestionId: question.id,
+ },
+ select: {
+ id: true,
+ inputType: {
+ select: {
+ name: true,
+ },
+ },
+ optionGroupId: true,
+ },
+ });
+ // popuate all subquestions
+ await Promise.all(
+ subQuestions.map((subq) => {
+ // assign subquestion optionGroupId to be same as parent, as it's null for subquestions
+ subq.optionGroupId = question.optionGroupId;
+ populateQuestionResponses(
+ subq,
+ teamMemberId,
+ responseGroupId,
+ );
+ }),
+ );
+ break;
+ }
+ case "checkbox": {
+ // check 2 randomly,
+ // 2 response entries as scalar list or optional list is not supported for relations
+ const checkboxChoices = await getRandomOptionId(
+ question.optionGroupId,
+ 2,
+ );
+
+ await prisma.response.createMany({
+ data: [
+ {
+ ...data,
+ optionChoiceId: checkboxChoices[0],
+ },
+ {
+ ...data,
+ optionChoiceId: checkboxChoices[1],
+ },
+ ],
+ });
+ break;
+ }
+ case "teamMembersCheckbox": {
+ const selectedTeamMembers = await getTeamMembers(teamMemberId);
+ await prisma.response.create({
+ data: {
+ ...data,
+ text: selectedTeamMembers
+ .filter((m) => m !== null)
+ .join(";"),
+ },
+ });
+ break;
+ }
+ case "number": {
+ await prisma.response.create({
+ data: {
+ ...data,
+ numeric: Math.floor(Math.random() * 100),
+ },
+ });
+ break;
+ }
+ case "yesNo": {
+ await prisma.response.create({
+ data: {
+ ...data,
+ boolean: Math.random() > 0.5,
+ },
+ });
+ break;
+ }
+ default: {
+ throw new Error("Prisma seed: Unexpected question type");
+ }
+ }
+};
+
+export const populateCheckinFormResponse = async () => {
+ const teamMemberId = 1;
+ const teamMember = await prisma.voyageTeamMember.findUnique({
+ where: {
+ id: teamMemberId,
+ },
+ select: {
+ id: true,
+ voyageTeam: {
+ select: {
+ voyage: {
+ select: {
+ sprints: true,
+ },
+ },
+ },
+ },
+ },
+ });
+
+ // get questions
+ const checkinForm = await prisma.form.findUnique({
+ where: {
+ title: "Sprint Check-in",
+ },
+ select: formSelect,
+ });
+
+ const questions = await prisma.question.findMany({
+ where: {
+ formId: checkinForm.id,
+ parentQuestionId: null,
+ },
+ select: {
+ id: true,
+ order: true,
+ inputType: {
+ select: {
+ name: true,
+ },
+ },
+ optionGroupId: true,
+ parentQuestionId: true,
+ },
+ orderBy: {
+ order: "asc",
+ },
+ });
+
+ const responseGroup = await prisma.responseGroup.create({
+ data: {},
+ });
+
+ await Promise.all(
+ questions.map((question) => {
+ populateQuestionResponses(question, teamMemberId, responseGroup.id);
+ }),
+ );
+
+ await prisma.formResponseCheckin.create({
+ data: {
+ voyageTeamMemberId: teamMember.id,
+ sprintId: teamMember.voyageTeam.voyage.sprints[0].id,
+ responseGroupId: responseGroup.id,
+ },
+ });
+};
diff --git a/prisma/seed/checklist.ts b/prisma/seed/checklist.ts
index dc9b8c95..be5e864c 100644
--- a/prisma/seed/checklist.ts
+++ b/prisma/seed/checklist.ts
@@ -1,5 +1,4 @@
-import { PrismaClient } from "@prisma/client";
-const prisma = new PrismaClient();
+import { prisma } from "./prisma-client";
export const populateChecklists = async () => {
await prisma.checklistItem.createMany({
diff --git a/prisma/seed/forms/checkinform.ts b/prisma/seed/forms/checkinform.ts
index c69ec5a4..2d86f06c 100644
--- a/prisma/seed/forms/checkinform.ts
+++ b/prisma/seed/forms/checkinform.ts
@@ -1,6 +1,4 @@
-import { PrismaClient } from "@prisma/client";
-
-const prisma = new PrismaClient();
+import { prisma } from "../prisma-client";
export const populateCheckinForm = async () => {
// Sprint - checkin form
diff --git a/prisma/seed/forms/index.ts b/prisma/seed/forms/index.ts
index 449b0400..ffabd09e 100644
--- a/prisma/seed/forms/index.ts
+++ b/prisma/seed/forms/index.ts
@@ -1,9 +1,7 @@
-import { PrismaClient } from "@prisma/client";
import { populateCheckinForm } from "./checkinform";
import { populateSoloProjectForm } from "./solo-project";
import { populateVoyageApplicationForm } from "./voyage-app";
-
-const prisma = new PrismaClient();
+import { prisma } from "../prisma-client";
export const populateFormsAndResponses = async () => {
// test option choices for Voyage Application form
diff --git a/prisma/seed/forms/solo-project.ts b/prisma/seed/forms/solo-project.ts
index 150db6a1..e59fd9a7 100644
--- a/prisma/seed/forms/solo-project.ts
+++ b/prisma/seed/forms/solo-project.ts
@@ -1,8 +1,6 @@
-import { PrismaClient } from "@prisma/client";
-
-const prisma = new PrismaClient();
-
// TODO: this is incomplete. just added some fields for testing
+import { prisma } from "../prisma-client";
+
export const populateSoloProjectForm = async () => {
await prisma.form.create({
data: {
diff --git a/prisma/seed/forms/voyage-app.ts b/prisma/seed/forms/voyage-app.ts
index a12ac1de..2849344d 100644
--- a/prisma/seed/forms/voyage-app.ts
+++ b/prisma/seed/forms/voyage-app.ts
@@ -1,8 +1,6 @@
-import { PrismaClient } from "@prisma/client";
-
-const prisma = new PrismaClient();
-
// TODO: this is incomplete. just added some fields for testing
+import { prisma } from "../prisma-client";
+
export const populateVoyageApplicationForm = async () => {
await prisma.form.create({
data: {
diff --git a/prisma/seed/index.ts b/prisma/seed/index.ts
index 35515357..7538774f 100644
--- a/prisma/seed/index.ts
+++ b/prisma/seed/index.ts
@@ -1,7 +1,4 @@
import { seed } from "./seed";
-import { PrismaClient } from "@prisma/client";
-
-const prisma = new PrismaClient();
(async function () {
try {
@@ -9,7 +6,5 @@ const prisma = new PrismaClient();
} catch (e) {
console.error(e);
process.exit(1);
- } finally {
- await prisma.$disconnect();
}
})();
diff --git a/prisma/seed/meetings.ts b/prisma/seed/meetings.ts
index 478af630..ac504302 100644
--- a/prisma/seed/meetings.ts
+++ b/prisma/seed/meetings.ts
@@ -1,6 +1,5 @@
import { getRandomDateDuringSprint, getSprintId } from "./utils";
-import { PrismaClient } from "@prisma/client";
-const prisma = new PrismaClient();
+import { prisma } from "./prisma-client";
export const populateMeetings = async () => {
// connect teamMeetings and form id
diff --git a/prisma/seed/prisma-client.ts b/prisma/seed/prisma-client.ts
new file mode 100644
index 00000000..901f3a0d
--- /dev/null
+++ b/prisma/seed/prisma-client.ts
@@ -0,0 +1,3 @@
+import { PrismaClient } from "@prisma/client";
+
+export const prisma = new PrismaClient();
diff --git a/prisma/seed/resources-project-ideas.ts b/prisma/seed/resources-project-ideas.ts
index 7c2b69bf..a46f9312 100644
--- a/prisma/seed/resources-project-ideas.ts
+++ b/prisma/seed/resources-project-ideas.ts
@@ -1,7 +1,6 @@
-import { PrismaClient } from "@prisma/client";
-const prisma = new PrismaClient();
-
// VoyageTeams, VoyageMembers, Tech Stack, sprints
+import { prisma } from "./prisma-client";
+
export const populateTeamResourcesAndProjectIdeas = async () => {
const voyageTeamMembers = await prisma.voyageTeamMember.findMany({});
diff --git a/prisma/seed/seed.ts b/prisma/seed/seed.ts
index 33b9b057..3617614e 100644
--- a/prisma/seed/seed.ts
+++ b/prisma/seed/seed.ts
@@ -9,9 +9,8 @@ import { populateMeetings } from "./meetings";
import { populateSoloProjects } from "./solo-project";
import { populateVoyageApplications } from "./voyage-app";
import { populateChecklists } from "./checklist";
-import { PrismaClient } from "@prisma/client";
-
-const prisma = new PrismaClient();
+import { populateCheckinFormResponse } from "./checkinform-responses";
+import { prisma } from "./prisma-client";
export const deleteAllTables = async () => {
const tablenames = await prisma.$queryRaw<
@@ -47,5 +46,8 @@ export const seed = async () => {
await populateSoloProjects();
await populateVoyageApplications();
await populateChecklists();
+ await populateCheckinFormResponse();
console.log("===\n🌱 Database seeding completed.\n===");
+
+ prisma.$disconnect();
};
diff --git a/prisma/seed/solo-project.ts b/prisma/seed/solo-project.ts
index b9deac3d..4aa0f524 100644
--- a/prisma/seed/solo-project.ts
+++ b/prisma/seed/solo-project.ts
@@ -1,7 +1,5 @@
-import { PrismaClient } from "@prisma/client";
import { passedSampleFeedback } from "./data/text/solo-project-feedback";
-
-const prisma = new PrismaClient();
+import { prisma } from "./prisma-client";
export const populateSoloProjects = async () => {
// solo project status
diff --git a/prisma/seed/sprints.ts b/prisma/seed/sprints.ts
index 2cfb888e..ad449ea5 100644
--- a/prisma/seed/sprints.ts
+++ b/prisma/seed/sprints.ts
@@ -1,6 +1,5 @@
-import { PrismaClient } from "@prisma/client";
import { addDays } from "./utils";
-const prisma = new PrismaClient();
+import { prisma } from "./prisma-client";
export const populateSprints = async () => {
const voyages = await prisma.voyage.findMany({});
diff --git a/prisma/seed/tables.ts b/prisma/seed/tables.ts
index 72c70b69..39eb64d7 100644
--- a/prisma/seed/tables.ts
+++ b/prisma/seed/tables.ts
@@ -1,6 +1,3 @@
-import { PrismaClient } from "@prisma/client";
-const prisma = new PrismaClient();
-
import Genders from "./data/genders";
import Tiers from "./data/tiers";
import VoyageRoles from "./data/voyage-roles";
@@ -13,6 +10,7 @@ import FormTypes from "./data/form-types";
import InputTypes from "./data/input-types";
import OptionGroups from "./data/option-groups";
import Roles from "./data/roles";
+import { prisma } from "./prisma-client";
const populateTable = async (tableName: string, data) => {
await prisma[tableName].createMany({
diff --git a/prisma/seed/users.ts b/prisma/seed/users.ts
index efedd0bd..a16d29f5 100644
--- a/prisma/seed/users.ts
+++ b/prisma/seed/users.ts
@@ -1,7 +1,5 @@
-import { PrismaClient } from "@prisma/client";
import { hashPassword } from "../../src/utils/auth";
-
-const prisma = new PrismaClient();
+import { prisma } from "./prisma-client";
const getRoleId = (roles, name) => {
return roles.filter((role) => role.name == name)[0].id;
diff --git a/prisma/seed/utils/index.ts b/prisma/seed/utils/index.ts
index 6e426ead..9815ea02 100644
--- a/prisma/seed/utils/index.ts
+++ b/prisma/seed/utils/index.ts
@@ -1,5 +1,4 @@
-import { PrismaClient } from "@prisma/client";
-const prisma = new PrismaClient();
+import { prisma } from "../prisma-client";
export const addDays = (date, days) => {
const newDate = new Date(date);
diff --git a/prisma/seed/voyage-app.ts b/prisma/seed/voyage-app.ts
index 7a7bdfd7..ed12a44b 100644
--- a/prisma/seed/voyage-app.ts
+++ b/prisma/seed/voyage-app.ts
@@ -1,6 +1,4 @@
-import { PrismaClient } from "@prisma/client";
-
-const prisma = new PrismaClient();
+import { prisma } from "./prisma-client";
export const populateVoyageApplications = async () => {
const users = await prisma.user.findMany({});
diff --git a/prisma/seed/voyage-teams.ts b/prisma/seed/voyage-teams.ts
index 53085b0e..032334ae 100644
--- a/prisma/seed/voyage-teams.ts
+++ b/prisma/seed/voyage-teams.ts
@@ -1,6 +1,4 @@
-import { PrismaClient } from "@prisma/client";
-
-const prisma = new PrismaClient();
+import { prisma } from "./prisma-client";
export const populateVoyageTeams = async () => {
const users = await prisma.user.findMany({});
diff --git a/prisma/seed/voyage.ts b/prisma/seed/voyage.ts
index bf4ec4ad..b405de6f 100644
--- a/prisma/seed/voyage.ts
+++ b/prisma/seed/voyage.ts
@@ -1,6 +1,4 @@
-import { PrismaClient } from "@prisma/client";
-
-const prisma = new PrismaClient();
+import { prisma } from "./prisma-client";
export const populateVoyages = async () => {
await prisma.voyage.create({
diff --git a/src/forms/forms.service.ts b/src/forms/forms.service.ts
index 9f5ec4dd..10341b54 100644
--- a/src/forms/forms.service.ts
+++ b/src/forms/forms.service.ts
@@ -1,5 +1,6 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
+import { Prisma } from "@prisma/client";
export const formSelect = {
id: true,
@@ -12,6 +13,9 @@ export const formSelect = {
title: true,
description: true,
questions: {
+ orderBy: {
+ order: "asc",
+ },
select: {
id: true,
order: true,
@@ -37,7 +41,7 @@ export const formSelect = {
},
},
},
-};
+} as Prisma.FormSelect;
@Injectable()
export class FormsService {
diff --git a/src/global/constants/formTitles.ts b/src/global/constants/formTitles.ts
new file mode 100644
index 00000000..5f7b5da4
--- /dev/null
+++ b/src/global/constants/formTitles.ts
@@ -0,0 +1,7 @@
+export const FormTitles = {
+ sprintRetroAndReview: "Retrospective & Review",
+ sprintPlanning: "Sprint Planning",
+ sprintCheckin: "Sprint Check-in",
+ soloProjectSubmission: "Solo Project Submission Form",
+ voyageApplication: "Voyage Application Form",
+};
diff --git a/src/global/global.service.ts b/src/global/global.service.ts
index 4bba1abe..851378df 100644
--- a/src/global/global.service.ts
+++ b/src/global/global.service.ts
@@ -5,6 +5,7 @@ import {
} from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
import { CustomRequest } from "./types/CustomRequest";
+import { FormResponseDto } from "./dtos/FormResponse.dto";
@Injectable()
export class GlobalService {
@@ -42,4 +43,89 @@ export class GlobalService {
}
return teamMemberId;
}
+
+ // ======= FORM responses helper functions =====
+
+ // pass in any form response DTO, this will extract responses from the DTO,
+ // and parse it into and array for prisma bulk insert/update
+ public responseDtoToArray = (responses: any) => {
+ const responsesArray = [];
+ const responseIndex = ["response", "responses"];
+ for (const index in responses) {
+ if (responseIndex.includes(index)) {
+ responses[index].forEach((v: FormResponseDto) => {
+ responsesArray.push({
+ questionId: v.questionId,
+ ...(v.text ? { text: v.text } : { text: null }),
+ ...(v.numeric
+ ? { numeric: v.numeric }
+ : { numeric: null }),
+ ...(v.boolean
+ ? { boolean: v.boolean }
+ : { boolean: null }),
+ ...(v.optionChoiceId
+ ? { optionChoiceId: v.optionChoiceId }
+ : { optionChoiceId: null }),
+ });
+ });
+ }
+ }
+ return responsesArray;
+ };
+
+ // Checks that questions submitted for update match the form questions
+ // using the formId
+ public checkQuestionsInFormById = async (
+ formId: number,
+ responsesArray: FormResponseDto[],
+ ) => {
+ const form = await this.prisma.form.findUnique({
+ where: { id: formId },
+ select: {
+ title: true,
+ questions: {
+ select: {
+ id: true,
+ },
+ },
+ },
+ });
+
+ const questionIds = form.questions.flatMap((question) => question.id);
+
+ responsesArray.forEach((response) => {
+ if (questionIds.indexOf(response.questionId) === -1)
+ throw new BadRequestException(
+ `Question Id ${response.questionId} is not in form ${form.title} (id: ${formId})`,
+ );
+ });
+ };
+
+ // Checks that questions submitted for update match the form questions
+ // using the form title
+ public checkQuestionsInFormByTitle = async (
+ title: string,
+ responsesArray: FormResponseDto[],
+ ) => {
+ const form = await this.prisma.form.findUnique({
+ where: { title },
+ select: {
+ id: true,
+ questions: {
+ select: {
+ id: true,
+ },
+ },
+ },
+ });
+
+ const questionIds = form.questions.flatMap((question) => question.id);
+
+ responsesArray.forEach((response) => {
+ if (questionIds.indexOf(response.questionId) === -1)
+ throw new BadRequestException(
+ `Question Id ${response.questionId} is not in form ${title} (id: ${form.id})`,
+ );
+ });
+ };
}
diff --git a/src/global/types/CustomRequest.ts b/src/global/types/CustomRequest.ts
index d4609430..4bb22495 100644
--- a/src/global/types/CustomRequest.ts
+++ b/src/global/types/CustomRequest.ts
@@ -1,3 +1,5 @@
+import { Request } from "express";
+
type VoyageTeam = {
teamId: number;
memberId: number;
diff --git a/src/pipes/form-input-validation.ts b/src/pipes/form-input-validation.ts
index dd1068f6..d0440e18 100644
--- a/src/pipes/form-input-validation.ts
+++ b/src/pipes/form-input-validation.ts
@@ -13,6 +13,11 @@ export class FormInputValidationPipe implements PipeTransform {
`'responses' is not an array`,
);
value[index].forEach((v: FormResponseDto) => {
+ if (!v.questionId) {
+ throw new BadRequestException(
+ `Question id is missing for one of the responses.`,
+ );
+ }
if (
!v.text &&
!v.numeric &&
diff --git a/src/sprints/dto/create-checkin-form-response.dto.ts b/src/sprints/dto/create-checkin-form-response.dto.ts
deleted file mode 100644
index 7eb067a4..00000000
--- a/src/sprints/dto/create-checkin-form-response.dto.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { FormResponseDto } from "../../global/dtos/FormResponse.dto";
-
-export class CreateCheckinFormResponseDto {
- responses: FormResponseDto[];
-}
diff --git a/src/sprints/dto/create-checkin-form.dto.ts b/src/sprints/dto/create-checkin-form.dto.ts
new file mode 100644
index 00000000..f8c0e374
--- /dev/null
+++ b/src/sprints/dto/create-checkin-form.dto.ts
@@ -0,0 +1,29 @@
+import { FormResponseDto } from "../../global/dtos/FormResponse.dto";
+import { ApiProperty } from "@nestjs/swagger";
+import { IsNotEmpty } from "class-validator";
+
+export class CreateCheckinFormDto {
+ @ApiProperty({
+ description: "voyage team member id, not userId",
+ example: 1,
+ })
+ @IsNotEmpty()
+ voyageTeamMemberId: number;
+
+ @ApiProperty({
+ description: "sprint id, not sprint number",
+ example: 1,
+ })
+ @IsNotEmpty()
+ sprintId: number;
+
+ @ApiProperty({
+ description: "An array of form responses",
+ example: [
+ { questionId: 11, text: "All" },
+ { questionId: 12, text: "Deploy app" },
+ ],
+ })
+ @IsNotEmpty()
+ responses: FormResponseDto[];
+}
diff --git a/src/sprints/sprints.controller.ts b/src/sprints/sprints.controller.ts
index 9db3a324..a5f08364 100644
--- a/src/sprints/sprints.controller.ts
+++ b/src/sprints/sprints.controller.ts
@@ -20,6 +20,7 @@ import { FormInputValidationPipe } from "../pipes/form-input-validation";
import { UpdateMeetingFormResponseDto } from "./dto/update-meeting-form-response.dto";
import {
AgendaResponse,
+ CheckinSubmissionResponse,
MeetingFormResponse,
MeetingResponse,
MeetingResponseWithSprintAndAgenda,
@@ -29,8 +30,10 @@ import {
BadRequestErrorResponse,
ConflictErrorResponse,
NotFoundErrorResponse,
+ UnauthorizedErrorResponse,
} from "../global/responses/errors";
import { FormResponse, ResponseResponse } from "../forms/forms.response";
+import { CreateCheckinFormDto } from "./dto/create-checkin-form.dto";
@Controller()
@ApiTags("Voyage - Sprints")
@@ -418,4 +421,64 @@ export class SprintsController {
updateMeetingFormResponse,
);
}
+
+ @ApiOperation({
+ summary: "Submit end of sprint check in form",
+ description:
+ "Inputs (choiceId, text, boolean, number are all optional),
" +
+ "depends on the question type, but AT LEAST ONE of them must be present,
" +
+ "questionId is required
" +
+ "responses example" +
+ "" +
+ JSON.stringify([
+ {
+ questionId: 1,
+ text: "All",
+ },
+ {
+ questionId: 2,
+ optionChoiceId: 1,
+ },
+ {
+ questionId: 3,
+ boolean: true,
+ },
+ {
+ questionId: 4,
+ numeric: 352,
+ },
+ ]) +
+ "
",
+ })
+ @ApiResponse({
+ status: HttpStatus.CREATED,
+ description: "The check in form has been successfully Submitted.",
+ type: CheckinSubmissionResponse,
+ isArray: true,
+ })
+ @ApiResponse({
+ status: HttpStatus.BAD_REQUEST,
+ description:
+ "request body data error, e.g. missing question id, missing response inputs",
+ type: BadRequestErrorResponse,
+ })
+ @ApiResponse({
+ status: HttpStatus.UNAUTHORIZED,
+ description: "User is not logged in",
+ type: UnauthorizedErrorResponse,
+ })
+ @ApiResponse({
+ status: HttpStatus.CONFLICT,
+ description: "User has already submitted a check in for that sprint.",
+ type: ConflictErrorResponse,
+ })
+ @Post("check-in")
+ addCheckinFormResponse(
+ @Body(new FormInputValidationPipe())
+ createCheckinFormResponse: CreateCheckinFormDto,
+ ) {
+ return this.sprintsService.addCheckinFormResponse(
+ createCheckinFormResponse,
+ );
+ }
}
diff --git a/src/sprints/sprints.response.ts b/src/sprints/sprints.response.ts
index 3ed4f0b7..dabf6069 100644
--- a/src/sprints/sprints.response.ts
+++ b/src/sprints/sprints.response.ts
@@ -147,3 +147,20 @@ export class MeetingFormResponse {
@ApiProperty({ example: 5 })
responseGroupId: number;
}
+
+export class CheckinSubmissionResponse {
+ @ApiProperty({ example: 1 })
+ id: number;
+
+ @ApiProperty({ example: 1 })
+ voyageTeamMemberId: number;
+
+ @ApiProperty({ example: 1 })
+ sprintId: number;
+
+ @ApiProperty({ example: 1 })
+ responseGroupId: 5;
+
+ @ApiProperty({ example: "2023-11-30T06:47:11.694Z" })
+ createdAt: Date;
+}
diff --git a/src/sprints/sprints.service.ts b/src/sprints/sprints.service.ts
index ef26fad0..44c78154 100644
--- a/src/sprints/sprints.service.ts
+++ b/src/sprints/sprints.service.ts
@@ -9,45 +9,19 @@ import { PrismaService } from "../prisma/prisma.service";
import { CreateTeamMeetingDto } from "./dto/create-team-meeting.dto";
import { CreateAgendaDto } from "./dto/create-agenda.dto";
import { UpdateAgendaDto } from "./dto/update-agenda.dto";
-import { CreateMeetingFormResponseDto } from "./dto/create-meeting-form-response.dto";
import { FormsService } from "../forms/forms.service";
import { UpdateMeetingFormResponseDto } from "./dto/update-meeting-form-response.dto";
-import { FormResponseDto } from "../global/dtos/FormResponse.dto";
+import { CreateCheckinFormDto } from "./dto/create-checkin-form.dto";
+import { GlobalService } from "../global/global.service";
@Injectable()
export class SprintsService {
constructor(
private prisma: PrismaService,
private formServices: FormsService,
+ private globalServices: GlobalService,
) {}
- private responseDtoToArray = (
- responses: CreateMeetingFormResponseDto | UpdateMeetingFormResponseDto,
- ) => {
- const responsesArray = [];
- const responseIndex = ["response", "responses"];
- for (const index in responses) {
- if (responseIndex.includes(index)) {
- responses[index].forEach((v: FormResponseDto) => {
- responsesArray.push({
- questionId: v.questionId,
- ...(v.text ? { text: v.text } : { text: null }),
- ...(v.numeric
- ? { numeric: v.numeric }
- : { numeric: null }),
- ...(v.boolean
- ? { boolean: v.boolean }
- : { boolean: null }),
- ...(v.optionChoiceId
- ? { optionChoiceId: v.optionChoiceId }
- : { optionChoiceId: null }),
- });
- });
- }
- }
- return responsesArray;
- };
-
// this checks if the form with the given formId is of formType = "meeting"
private isMeetingForm = async (formId) => {
const form = await this.prisma.form.findUnique({
@@ -517,47 +491,101 @@ export class SprintsService {
);
}
- const responsesArray = this.responseDtoToArray(responses);
+ const responsesArray =
+ this.globalServices.responseDtoToArray(responses);
- // Checks that questions submitted for update match the form questions
- const form = await this.prisma.form.findUnique({
- where: { id: formId },
- select: {
- questions: {
- select: {
- id: true,
- },
- },
- },
- });
+ await this.globalServices.checkQuestionsInFormById(
+ formId,
+ responsesArray,
+ );
- const questionIds = form.questions.flatMap((question) => question.id);
+ return this.prisma.$transaction(
+ async (tx) =>
+ await Promise.all(
+ responsesArray.map(async (response) => {
+ const responseToUpdate = await tx.response.findFirst({
+ where: {
+ responseGroupId:
+ formResponseMeeting.responseGroupId,
+ questionId: response.questionId,
+ },
+ });
- responsesArray.forEach((response) => {
- if (questionIds.indexOf(response.questionId) === -1)
- throw new BadRequestException(
- `Question Id ${response.questionId} is not in form ${formId}`,
- );
- });
+ // if response does not already exist, update
+ // (this happens for a fresh meeting form)
+ // else create a new response
+ if (!responseToUpdate) {
+ return tx.response.create({
+ data: {
+ responseGroupId:
+ formResponseMeeting.responseGroupId,
+ ...response,
+ },
+ });
+ }
+ return tx.response.update({
+ where: {
+ id: responseToUpdate.id,
+ },
+ data: {
+ responseGroupId:
+ formResponseMeeting.responseGroupId,
+ ...response,
+ },
+ });
+ }),
+ ),
+ );
+ }
- return this.prisma.$transaction(
- responsesArray.map((response) => {
- const { questionId, ...data } = response;
- return this.prisma.response.upsert({
- where: {
- questionResponseGroup: {
- responseGroupId:
- formResponseMeeting.responseGroupId,
- questionId: response.questionId,
- },
- },
- update: data,
- create: {
- responseGroupId: formResponseMeeting.responseGroupId,
- ...response,
- },
- });
- }),
+ async addCheckinFormResponse(createCheckinForm: CreateCheckinFormDto) {
+ const responsesArray =
+ this.globalServices.responseDtoToArray(createCheckinForm);
+
+ await this.globalServices.checkQuestionsInFormByTitle(
+ "Sprint Check-in",
+ responsesArray,
);
+
+ // TODO: do we need to check if sprintID is a reasonable sprint Id?
+
+ try {
+ const checkinSubmission = await this.prisma.$transaction(
+ async (tx) => {
+ const responseGroup = await tx.responseGroup.create({
+ data: {
+ responses: {
+ createMany: {
+ data: responsesArray,
+ },
+ },
+ },
+ });
+ return tx.formResponseCheckin.create({
+ data: {
+ voyageTeamMemberId:
+ createCheckinForm.voyageTeamMemberId,
+ sprintId: createCheckinForm.sprintId,
+ responseGroupId: responseGroup.id,
+ },
+ });
+ },
+ );
+ return {
+ id: checkinSubmission.id,
+ voyageTeamMemberId: checkinSubmission.voyageTeamMemberId,
+ sprintId: checkinSubmission.sprintId,
+ responseGroupId: checkinSubmission.responseGroupId,
+ createdAt: checkinSubmission.createdAt,
+ };
+ } catch (e) {
+ if (e.code === "P2002") {
+ throw new ConflictException(
+ `User ${createCheckinForm.voyageTeamMemberId} has already submitted a checkin form for sprint id ${createCheckinForm.sprintId}.`,
+ );
+ } else {
+ console.log(e);
+ }
+ }
}
}
diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts
index f831194d..fd4d8693 100644
--- a/src/users/users.controller.ts
+++ b/src/users/users.controller.ts
@@ -11,12 +11,7 @@ import {
Request,
} from "@nestjs/common";
import { UsersService } from "./users.service";
-import {
- ApiOperation,
- ApiParam,
- ApiResponse,
- ApiTags,
-} from "@nestjs/swagger";
+import { ApiOperation, ApiParam, ApiResponse, ApiTags } from "@nestjs/swagger";
import { FullUserResponse, PrivateUserResponse } from "./users.response";
import {
@@ -129,12 +124,12 @@ export class UsersController {
description: "Given email is not in a valid email syntax.",
type: BadRequestErrorResponse,
})
-
@Roles(AppRoles.Admin)
@HttpCode(200)
@Post("/lookup-by-email")
- async getUserDetailsByEmail(@Body() userLookupByEmailDto: UserLookupByEmailDto) {
-
+ async getUserDetailsByEmail(
+ @Body() userLookupByEmailDto: UserLookupByEmailDto,
+ ) {
const userDetails =
await this.usersService.getUserDetailsByEmail(userLookupByEmailDto);
if (!userDetails) {
diff --git a/test/auth.e2e-spec.ts b/test/auth.e2e-spec.ts
index 69ab8f77..4adde236 100644
--- a/test/auth.e2e-spec.ts
+++ b/test/auth.e2e-spec.ts
@@ -4,7 +4,11 @@ import { AppModule } from "../src/app.module";
import { seed } from "../prisma/seed/seed";
import * as request from "supertest";
import * as cookieParser from "cookie-parser";
-import { extractCookieByKey, extractResCookieValueByKey } from "./utils";
+import {
+ extractCookieByKey,
+ extractResCookieValueByKey,
+ loginAndGetTokens,
+} from "./utils";
import { PrismaService } from "../src/prisma/prisma.service";
import { comparePassword } from "../src/utils/auth";
@@ -16,27 +20,6 @@ const verifyUrl = "/auth/verify-email";
const resetRequestUrl = "/auth/reset-password/request";
const resetPWUrl = "/auth/reset-password";
const revokeRTUrl = "/auth/refresh/revoke";
-const loginAndGetTokens = async (
- email: string,
- password: string,
- app: INestApplication,
-) => {
- const r = await request(app.getHttpServer()).post(loginUrl).send({
- email,
- password,
- });
-
- const access_token = extractCookieByKey(
- r.headers["set-cookie"],
- "access_token",
- );
- const refresh_token = extractCookieByKey(
- r.headers["set-cookie"],
- "refresh_token",
- );
-
- return { access_token, refresh_token };
-};
const getUserIdByEmail = async (email: string, prisma: PrismaService) => {
const user = await prisma.user.findUnique({
diff --git a/test/sprints.e2e-spec.ts b/test/sprints.e2e-spec.ts
index e9892c6b..a35a5708 100644
--- a/test/sprints.e2e-spec.ts
+++ b/test/sprints.e2e-spec.ts
@@ -4,32 +4,18 @@ import * as request from "supertest";
import { AppModule } from "../src/app.module";
import { PrismaService } from "../src/prisma/prisma.service";
import { seed } from "../prisma/seed/seed";
-import { extractResCookieValueByKey } from "./utils";
+import { loginAndGetTokens } from "./utils";
import { CreateAgendaDto } from "src/sprints/dto/create-agenda.dto";
import { toBeOneOf } from "jest-extended";
+import * as cookieParser from "cookie-parser";
+import { FormTitles } from "../src/global/constants/formTitles";
expect.extend({ toBeOneOf });
describe("Sprints Controller (e2e)", () => {
let app: INestApplication;
let prisma: PrismaService;
- let userAccessToken: string;
-
- async function loginUser() {
- await request(app.getHttpServer())
- .post("/auth/login")
- .send({
- email: "jessica.williamson@gmail.com",
- password: "password",
- })
- .expect(200)
- .then((res) => {
- userAccessToken = extractResCookieValueByKey(
- res.headers["set-cookie"],
- "access_token",
- );
- });
- }
+ let accessToken: any;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
@@ -40,6 +26,7 @@ describe("Sprints Controller (e2e)", () => {
app = moduleFixture.createNestApplication();
prisma = moduleFixture.get(PrismaService);
app.useGlobalPipes(new ValidationPipe());
+ app.use(cookieParser());
await app.init();
});
@@ -49,14 +36,20 @@ describe("Sprints Controller (e2e)", () => {
});
beforeEach(async () => {
- await loginUser();
+ await loginAndGetTokens(
+ "jessica.williamson@gmail.com",
+ "password",
+ app,
+ ).then((tokens) => {
+ accessToken = tokens.access_token;
+ });
});
describe("GET /voyages/sprints - gets all voyage and sprints data", () => {
it("should return 200 if fetching all voyage and sprints data", async () => {
return request(app.getHttpServer())
.get(`/voyages/sprints`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.expect(200)
.expect("Content-Type", /json/)
.expect((res) => {
@@ -107,7 +100,7 @@ describe("Sprints Controller (e2e)", () => {
const teamId = 1;
return request(app.getHttpServer())
.get(`/voyages/sprints/teams/${teamId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.expect(200)
.expect("Content-Type", /json/)
.expect((res) => {
@@ -136,7 +129,7 @@ describe("Sprints Controller (e2e)", () => {
const teamId = 9999;
return request(app.getHttpServer())
.get(`/voyages/sprints/teams/${teamId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.expect(404);
});
@@ -154,7 +147,7 @@ describe("Sprints Controller (e2e)", () => {
const meetingId = 1;
return request(app.getHttpServer())
.get(`/voyages/sprints/meetings/${meetingId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.expect(200)
.expect("Content-Type", /json/)
.expect((res) => {
@@ -235,7 +228,7 @@ describe("Sprints Controller (e2e)", () => {
const meetingId = 9999;
return request(app.getHttpServer())
.get(`/voyages/sprints/meetings/${meetingId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.expect(404);
});
@@ -253,7 +246,7 @@ describe("Sprints Controller (e2e)", () => {
const meetingId = 1;
return request(app.getHttpServer())
.patch(`/voyages/sprints/meetings/${meetingId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.send({
title: "Test title",
dateTime: "2024-02-29T17:17:50.100Z",
@@ -297,7 +290,7 @@ describe("Sprints Controller (e2e)", () => {
.post(
`/voyages/sprints/${sprintNumber}/teams/${teamId}/meetings`,
)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.send({
title: "Sprint Planning",
dateTime: "2024-03-01T23:11:20.271Z",
@@ -339,7 +332,7 @@ describe("Sprints Controller (e2e)", () => {
.post(
`/voyages/sprints/${sprintNumber}/teams/${teamId}/meetings`,
)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.send({
title: "Sprint Planning",
dateTime: "2024-03-01T23:11:20.271Z",
@@ -356,7 +349,7 @@ describe("Sprints Controller (e2e)", () => {
.post(
`/voyages/sprints/${sprintNumber}/teams/${teamId}/meetings`,
)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.send({
title: "Sprint Planning",
dateTime: "2024-03-01T23:11:20.271Z",
@@ -373,7 +366,7 @@ describe("Sprints Controller (e2e)", () => {
.post(
`/voyages/sprints/${sprintNumber}/teams/${teamId}/meetings`,
)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.send({
title: 1, //bad request - title should be string
dateTime: "2024-03-01T23:11:20.271Z",
@@ -394,7 +387,7 @@ describe("Sprints Controller (e2e)", () => {
};
return request(app.getHttpServer())
.post(`/voyages/sprints/meetings/${meetingId}/agendas`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.send(createAgendaDto)
.expect(201)
.expect((res) => {
@@ -426,7 +419,7 @@ describe("Sprints Controller (e2e)", () => {
const meetingId = " ";
return request(app.getHttpServer())
.post(`/voyages/sprints/meetings/${meetingId}/agendas`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.send({
title: "Contribute to the agenda!",
description:
@@ -441,7 +434,7 @@ describe("Sprints Controller (e2e)", () => {
const agendaId = 1;
return request(app.getHttpServer())
.patch(`/voyages/sprints/agendas/${agendaId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.send({
title: "Title updated",
description: "New agenda",
@@ -477,7 +470,7 @@ describe("Sprints Controller (e2e)", () => {
const agendaId = 9999;
return request(app.getHttpServer())
.patch(`/voyages/sprints/agendas/${agendaId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.send({
title: "Title updated",
description: "New agenda",
@@ -491,7 +484,7 @@ describe("Sprints Controller (e2e)", () => {
const agendaId = 1;
return request(app.getHttpServer())
.delete(`/voyages/sprints/agendas/${agendaId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.expect(200)
.expect((res) => {
expect(res.body).toEqual(
@@ -520,7 +513,7 @@ describe("Sprints Controller (e2e)", () => {
const agendaId = 9999;
return request(app.getHttpServer())
.delete(`/voyages/sprints/agendas/${agendaId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.expect(404);
});
});
@@ -531,7 +524,7 @@ describe("Sprints Controller (e2e)", () => {
const formId = 1;
return request(app.getHttpServer())
.post(`/voyages/sprints/meetings/${meetingId}/forms/${formId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.expect(201)
.expect((res) => {
expect(res.body).toEqual(
@@ -562,7 +555,7 @@ describe("Sprints Controller (e2e)", () => {
const formId = 1;
return request(app.getHttpServer())
.post(`/voyages/sprints/meetings/${meetingId}/forms/${formId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.expect(409);
});
@@ -571,7 +564,7 @@ describe("Sprints Controller (e2e)", () => {
const formId = 1;
return request(app.getHttpServer())
.post(`/voyages/sprints/meetings/${meetingId}/forms/${formId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.expect(400);
});
@@ -580,7 +573,7 @@ describe("Sprints Controller (e2e)", () => {
const formId = 999;
return request(app.getHttpServer())
.post(`/voyages/sprints/meetings/${meetingId}/forms/${formId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.expect(400);
});
});
@@ -590,7 +583,7 @@ describe("Sprints Controller (e2e)", () => {
const formId = 1;
return request(app.getHttpServer())
.get(`/voyages/sprints/meetings/${meetingId}/forms/${formId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.expect(200)
.expect((res) => {
expect(res.body).toEqual(
@@ -628,7 +621,7 @@ describe("Sprints Controller (e2e)", () => {
const formId = 1;
return request(app.getHttpServer())
.get(`/voyages/sprints/meetings/${meetingId}/forms/${formId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.expect(404);
});
@@ -637,25 +630,22 @@ describe("Sprints Controller (e2e)", () => {
const formId = 9999;
return request(app.getHttpServer())
.get(`/voyages/sprints/meetings/${meetingId}/forms/${formId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.expect(400);
});
});
describe("PATCH /voyages/sprints/meetings/:meetingId/forms/:formId - updates a meeting form", () => {
- it("should return 200 if successfully update the meeting form with responses", async () => {
+ it("should return 200 if successfully create a meeting form response", async () => {
const meetingId = 1;
const formId = 1;
return request(app.getHttpServer())
.patch(`/voyages/sprints/meetings/${meetingId}/forms/${formId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.send({
responses: [
{
questionId: 1,
- optionChoiceId: 1,
text: "Team member x landed a job this week.",
- boolean: true,
- number: 1,
},
],
})
@@ -666,12 +656,6 @@ describe("Sprints Controller (e2e)", () => {
expect.objectContaining({
id: expect.any(Number),
questionId: expect.any(Number),
- optionChoiceId: expect.any(Number),
- numeric: expect.toBeOneOf([
- null,
- expect.any(Number),
- ]),
- boolean: expect.any(Boolean),
text: expect.any(String),
responseGroupId: expect.any(Number),
createdAt: expect.any(String),
@@ -681,15 +665,54 @@ describe("Sprints Controller (e2e)", () => {
);
});
});
- it("- verify meeting response found in database", async () => {
+ it("- verify meeting response found in database (create)", async () => {
const response = await prisma.response.findMany({
where: {
questionId: 1,
- optionChoiceId: 1,
text: "Team member x landed a job this week.",
- boolean: true,
},
});
+ expect(response.length).toBe(1);
+ return expect(response[0].questionId).toEqual(1);
+ });
+ it("should return 200 if successfully update a meeting form response", async () => {
+ const meetingId = 1;
+ const formId = 1;
+ return request(app.getHttpServer())
+ .patch(`/voyages/sprints/meetings/${meetingId}/forms/${formId}`)
+ .set("Cookie", accessToken)
+ .send({
+ responses: [
+ {
+ questionId: 1,
+ text: "Update - Team member x landed a job this week.",
+ },
+ ],
+ })
+ .expect(200)
+ .expect((res) => {
+ expect(res.body).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: expect.any(Number),
+ questionId: expect.any(Number),
+ text: expect.any(String),
+ responseGroupId: expect.any(Number),
+ createdAt: expect.any(String),
+ updatedAt: expect.any(String),
+ }),
+ ]),
+ );
+ });
+ });
+ it("- verify meeting response is updated database", async () => {
+ const response = await prisma.response.findMany({
+ where: {
+ questionId: 1,
+ text: "Update - Team member x landed a job this week.",
+ },
+ });
+ expect(response.length).toBe(1);
return expect(response[0].questionId).toEqual(1);
});
@@ -698,7 +721,7 @@ describe("Sprints Controller (e2e)", () => {
const formId = "Bad request";
return request(app.getHttpServer())
.patch(`/voyages/sprints/meetings/${meetingId}/forms/${formId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.send({
responses: [
{
@@ -718,7 +741,7 @@ describe("Sprints Controller (e2e)", () => {
const formId = 1;
return request(app.getHttpServer())
.patch(`/voyages/sprints/meetings/${meetingId}/forms/${formId}`)
- .set("Authorization", `Bearer ${userAccessToken}`)
+ .set("Cookie", accessToken)
.send({
responses: {
questionId: 1,
@@ -731,4 +754,262 @@ describe("Sprints Controller (e2e)", () => {
.expect(400);
});
});
+
+ describe("POST /voyages/sprints/check-in - submit sprint check in form", () => {
+ const sprintCheckinUrl = "/voyages/sprints/check-in";
+ let checkinForm: any;
+ let questions: any;
+
+ beforeEach(async () => {
+ checkinForm = await prisma.form.findUnique({
+ where: {
+ title: FormTitles.sprintCheckin,
+ },
+ });
+ questions = await prisma.question.findMany({
+ where: {
+ formId: checkinForm.id,
+ },
+ select: {
+ id: true,
+ },
+ });
+ });
+
+ it("should return 201 if successfully submitted a check in form", async () => {
+ const responsesBefore = await prisma.response.count();
+ const responseGroupBefore = await prisma.responseGroup.count();
+ const checkinsBefore = await prisma.formResponseCheckin.count();
+
+ await request(app.getHttpServer())
+ .post(sprintCheckinUrl)
+ .set("Cookie", accessToken)
+ .send({
+ voyageTeamMemberId: 2, // voyageTeamMemberId 1 is already in the seed
+ sprintId: 1,
+ responses: [
+ {
+ questionId: questions[0].id,
+ text: "Text input value",
+ },
+ {
+ questionId: questions[1].id,
+ boolean: true,
+ },
+ {
+ questionId: questions[2].id,
+ numeric: 12,
+ },
+ {
+ questionId: questions[3].id,
+ optionChoiceId: 1,
+ },
+ ],
+ })
+ .expect(201);
+
+ const responsesAfter = await prisma.response.count();
+ const responseGroupAfter = await prisma.responseGroup.count();
+ const checkinsAfter = await prisma.formResponseCheckin.count();
+
+ expect(responsesAfter).toEqual(responsesBefore + 4);
+ expect(responseGroupAfter).toEqual(responseGroupBefore + 1);
+ expect(checkinsAfter).toEqual(checkinsBefore + 1);
+ });
+ it("should return 400 for invalid inputs", async () => {
+ const responsesBefore = await prisma.response.count();
+ const responseGroupBefore = await prisma.responseGroup.count();
+ const checkinsBefore = await prisma.formResponseCheckin.count();
+ // missing voyageTeamMemberId
+ await request(app.getHttpServer())
+ .post(sprintCheckinUrl)
+ .set("Cookie", accessToken)
+ .send({
+ sprintId: 1,
+ responses: [
+ {
+ questionId: questions[0].id,
+ text: "Text input value",
+ },
+ ],
+ })
+ .expect(400);
+
+ // missing sprintId"
+ await request(app.getHttpServer())
+ .post(sprintCheckinUrl)
+ .set("Cookie", accessToken)
+ .send({
+ voyageTeamMemberId: 1,
+ responses: [
+ {
+ questionId: questions[0].id,
+ text: "Text input value",
+ },
+ ],
+ })
+ .expect(400);
+
+ // missing responses
+ await request(app.getHttpServer())
+ .post(sprintCheckinUrl)
+ .set("Cookie", accessToken)
+ .send({
+ voyageTeamMemberId: 1,
+ sprintId: 1,
+ })
+ .expect(400);
+
+ // missing questionId in responses - response validation pipe
+ await request(app.getHttpServer())
+ .post(sprintCheckinUrl)
+ .set("Cookie", accessToken)
+ .send({
+ voyageTeamMemberId: 1,
+ responses: [
+ {
+ text: "Text input value",
+ },
+ ],
+ })
+ .expect(400);
+
+ // missing input in responses - response validation pipe
+ await request(app.getHttpServer())
+ .post(sprintCheckinUrl)
+ .set("Cookie", accessToken)
+ .send({
+ voyageTeamMemberId: 1,
+ responses: [
+ {
+ questionId: questions[0].id,
+ },
+ ],
+ })
+ .expect(400);
+
+ // wrong response input types
+ await request(app.getHttpServer())
+ .post(sprintCheckinUrl)
+ .set("Cookie", accessToken)
+ .send({
+ voyageTeamMemberId: 1,
+ responses: [
+ {
+ questionId: questions[0].id,
+ numeric: "not a number",
+ },
+ ],
+ })
+ .expect(400);
+
+ await request(app.getHttpServer())
+ .post(sprintCheckinUrl)
+ .set("Cookie", accessToken)
+ .send({
+ voyageTeamMemberId: 1,
+ responses: [
+ {
+ questionId: questions[0].id,
+ responseGroupId: "not an id",
+ },
+ ],
+ })
+ .expect(400);
+
+ await request(app.getHttpServer())
+ .post(sprintCheckinUrl)
+ .set("Cookie", accessToken)
+ .send({
+ voyageTeamMemberId: 1,
+ responses: [
+ {
+ questionId: questions[0].id,
+ optionGroupId: "not an id",
+ },
+ ],
+ })
+ .expect(400);
+
+ await request(app.getHttpServer())
+ .post(sprintCheckinUrl)
+ .set("Cookie", accessToken)
+ .send({
+ voyageTeamMemberId: 1,
+ responses: [
+ {
+ questionId: questions[0].id,
+ boolean: "not a boolean",
+ },
+ ],
+ })
+ .expect(400);
+
+ const responsesAfter = await prisma.response.count();
+ const responseGroupAfter = await prisma.responseGroup.count();
+ const checkinsAfter = await prisma.formResponseCheckin.count();
+
+ expect(responsesAfter).toEqual(responsesBefore);
+ expect(responseGroupAfter).toEqual(responseGroupBefore);
+ expect(checkinsAfter).toEqual(checkinsBefore);
+ });
+
+ it("should return 401 if user is not logged in", async () => {
+ await request(app.getHttpServer())
+ .post(sprintCheckinUrl)
+ .send({
+ voyageTeamMemberId: 1,
+ sprintId: 1,
+ responses: [
+ {
+ questionId: questions[0].id,
+ text: "Text input value",
+ },
+ ],
+ })
+ .expect(401);
+ });
+
+ it("should return 409 if user has already submitted the check in form for the same sprint", async () => {
+ await request(app.getHttpServer())
+ .post(sprintCheckinUrl)
+ .set("Cookie", accessToken)
+ .send({
+ voyageTeamMemberId: 1,
+ sprintId: 1,
+ responses: [
+ {
+ questionId: questions[0].id,
+ text: "Text input value",
+ },
+ ],
+ });
+ const responsesBefore = await prisma.response.count();
+ const responseGroupBefore = await prisma.responseGroup.count();
+ const checkinsBefore = await prisma.formResponseCheckin.count();
+
+ await request(app.getHttpServer())
+ .post(sprintCheckinUrl)
+ .set("Cookie", accessToken)
+ .send({
+ voyageTeamMemberId: 1,
+ sprintId: 1,
+ responses: [
+ {
+ questionId: questions[0].id,
+ text: "Text input value",
+ },
+ ],
+ })
+ .expect(409);
+
+ const responsesAfter = await prisma.response.count();
+ const responseGroupAfter = await prisma.responseGroup.count();
+ const checkinsAfter = await prisma.formResponseCheckin.count();
+
+ expect(responsesAfter).toEqual(responsesBefore);
+ expect(responseGroupAfter).toEqual(responseGroupBefore);
+ expect(checkinsAfter).toEqual(checkinsBefore);
+ });
+ });
});
diff --git a/test/utils.ts b/test/utils.ts
index aaf97d99..7db4c5e2 100644
--- a/test/utils.ts
+++ b/test/utils.ts
@@ -41,18 +41,24 @@ export const loginAndGetTokens = async (
};
export const getNonAdminUser = async () => {
- const adminRole = await prisma.role.findUnique({
- where: {
- name: "admin",
- },
- });
- return prisma.user.findFirst({
- where: {
- roles: {
- none: {
- roleId: adminRole.id,
+ try {
+ const adminRole = await prisma.role.findUnique({
+ where: {
+ name: "admin",
+ },
+ });
+ return prisma.user.findFirst({
+ where: {
+ roles: {
+ none: {
+ roleId: adminRole.id,
+ },
},
},
- },
- });
+ });
+ } catch (e) {
+ console.log(e);
+ } finally {
+ await prisma.$disconnect();
+ }
};