diff --git a/api/.nvmrc b/api/.nvmrc index ea438c37..d3e33db8 100644 --- a/api/.nvmrc +++ b/api/.nvmrc @@ -1 +1 @@ -v18.14 +v18.16 diff --git a/api/.yarnrc b/api/.yarnrc new file mode 100644 index 00000000..3b7b0fb6 --- /dev/null +++ b/api/.yarnrc @@ -0,0 +1 @@ +workspaces-experimental false diff --git a/api/Dockerfile b/api/Dockerfile index 7350cc54..caa11372 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,11 +1,11 @@ -FROM node:18.14-alpine AS build +FROM node:18.16-alpine AS build WORKDIR /app/ COPY package.json yarn.lock ./ RUN yarn install --ignore-platform --frozen-lockfile --network-timeout 600000 COPY ./ ./ RUN yarn build -FROM node:18.14-alpine +FROM node:18.16-alpine WORKDIR /app/ COPY --from=build /app/dist/ ./dist/ COPY --from=build /app/package.json /app/yarn.lock ./ diff --git a/api/package.json b/api/package.json index 25bf673b..8a427344 100644 --- a/api/package.json +++ b/api/package.json @@ -1,9 +1,9 @@ { "name": "@rhizone-lms/api", - "version": "0.0.0", + "version": "0.5.0", "private": true, "engines": { - "node": "^18.14.0", + "node": "^18.16.0", "yarn": "^1.22.19" }, "scripts": { @@ -19,44 +19,43 @@ "test:watch": "jest --watchAll" }, "dependencies": { - "connect-redis": "^6.0.0", + "connect-redis": "^7.1.0", "cors": "^2.8.5", "express": "^4.17.1", "express-session": "^1.17.2", - "helmet": "^6.0.1", + "helmet": "^7.0.0", "knex": "^2.4.2", - "luxon": "^3.2.1", - "mysql": "^2.18.1", - "redis": "^3.1.2", + "luxon": "^3.3.0", + "mysql2": "^3.3.1", + "redis": "^4.6.6", "rollbar": "^2.26.1", - "socket.io": "^4.6.0", + "socket.io": "^4.6.1", "superagent": "^8.0.9" }, "devDependencies": { - "@types/connect-redis": "^0.0.19", "@types/cors": "^2.8.13", "@types/express": "^4.17.17", - "@types/express-session": "^1.17.4", - "@types/jest": "^29.4.0", - "@types/luxon": "^3.2.0", - "@types/mock-knex": "^0.4.3", + "@types/express-session": "^1.17.7", + "@types/jest": "^29.5.1", + "@types/luxon": "^3.3.0", + "@types/mock-knex": "^0.4.5", "@types/mysql": "^2.15.19", - "@types/node": "^18.13.0", - "@types/superagent": "^4.1.13", + "@types/node": "^20.2.3", + "@types/superagent": "^4.1.17", "@types/supertest": "^2.0.11", - "@typescript-eslint/eslint-plugin": "^5.51.0", - "@typescript-eslint/parser": "^5.51.0", + "@typescript-eslint/eslint-plugin": "^5.59.7", + "@typescript-eslint/parser": "^5.59.7", "dotenv": "^16.0.3", - "eslint": "^8.33.0", - "eslint-config-prettier": "^8.6.0", - "jest": "^29.4.2", + "eslint": "^8.41.0", + "eslint-config-prettier": "^8.8.0", + "jest": "^29.5.0", "mock-knex": "^0.4.12", - "nodemon": "^2.0.15", - "prettier": "^2.8.4", + "nodemon": "^2.0.22", + "prettier": "^2.8.8", "supertest": "^6.3.3", - "ts-jest": "^29.0.5", + "ts-jest": "^29.1.0", "ts-node": "^10.4.0", - "typescript": "^4.9.5" + "typescript": "^5.0.4" }, "eslintConfig": { "env": { diff --git a/api/src/middleware/__tests__/assessmentsRouter.ts b/api/src/middleware/__tests__/assessmentsRouter.ts new file mode 100644 index 00000000..15face38 --- /dev/null +++ b/api/src/middleware/__tests__/assessmentsRouter.ts @@ -0,0 +1,3345 @@ +import { + collectionEnvelope, + errorEnvelope, + itemEnvelope, +} from '../responseEnvelope'; +import { createAppAgentForRouter, mockPrincipalId } from '../routerTestUtils'; + +import { + AssessmentDetails, + AssessmentSubmission, + AssessmentWithSubmissions, + AssessmentWithSummary, + CurriculumAssessment, + FacilitatorAssessmentSubmissionsSummary, + ParticipantAssessmentSubmissionsSummary, + ProgramAssessment, + SavedAssessment, +} from '../../models'; +import { + constructFacilitatorAssessmentSummary, + constructParticipantAssessmentSummary, + createAssessmentSubmission, + createCurriculumAssessment, + createProgramAssessment, + deleteCurriculumAssessment, + deleteProgramAssessment, + enrollFacilitator, + enrollParticipant, + facilitatorProgramIdsMatchingCurriculum, + findProgramAssessment, + getAssessmentSubmission, + getCurriculumAssessment, + getPrincipalProgramRole, + listAllProgramAssessmentSubmissions, + listParticipantProgramAssessmentSubmissions, + listPrincipalEnrolledProgramIds, + listProgramAssessments, + updateAssessmentSubmission, + updateCurriculumAssessment, + updateProgramAssessment, +} from '../../services/assessmentsService'; + +import assessmentsRouter from '../assessmentsRouter'; + +jest.mock('../../services/assessmentsService'); + +const mockConstructFacilitatorAssessmentSummary = jest.mocked( + constructFacilitatorAssessmentSummary +); +const mockConstructParticipantAssessmentSummary = jest.mocked( + constructParticipantAssessmentSummary +); +const mockCreateAssessmentSubmission = jest.mocked(createAssessmentSubmission); +const mockCreateCurriculumAssessment = jest.mocked(createCurriculumAssessment); +const mockCreateProgramAssessment = jest.mocked(createProgramAssessment); +const mockDeleteCurriculumAssessment = jest.mocked(deleteCurriculumAssessment); +const mockDeleteProgramAssessment = jest.mocked(deleteProgramAssessment); +const mockFindProgramAssessment = jest.mocked(findProgramAssessment); +const mockEnrollFacilitator = jest.mocked(enrollFacilitator); +const mockEnrollParticipant = jest.mocked(enrollParticipant); +const mockGetAssessmentSubmission = jest.mocked(getAssessmentSubmission); +const mockGetCurriculumAssessment = jest.mocked(getCurriculumAssessment); +const mockFacilitatorProgramIdsMatchingCurriculum = jest.mocked( + facilitatorProgramIdsMatchingCurriculum +); +const mockGetPrincipalProgramRole = jest.mocked(getPrincipalProgramRole); +const mockListAllProgramAssessmentSubmissions = jest.mocked( + listAllProgramAssessmentSubmissions +); +const mockListParticipantProgramAssessmentSubmissions = jest.mocked( + listParticipantProgramAssessmentSubmissions +); +const mockListPrincipalEnrolledProgramIds = jest.mocked( + listPrincipalEnrolledProgramIds +); +const mockListProgramAssessments = jest.mocked(listProgramAssessments); +const mockUpdateAssessmentSubmission = jest.mocked(updateAssessmentSubmission); +const mockUpdateCurriculumAssessment = jest.mocked(updateCurriculumAssessment); +const mockUpdateProgramAssessment = jest.mocked(updateProgramAssessment); + +/* EXAMPLE DATA: Variables */ + +const participantPrincipalId = 30; +const unenrolledPrincipalId = 31; +const otherParticipantPrincipalId = 32; +const facilitatorPrincipalId = 300; +const curriculumId = 4; +const curriculumAssessmentId = 8; +const assessmentSubmissionId = 32; +const facilitatorProgramIdsThatMatchCurriculum = [12, 20, 30]; + +/* EXAMPLE DATA: Database Rows */ + +const programAssessmentsRows = [ + { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06 00:00:00', + due_date: '2050-06-24 00:00:00', + }, + { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06 00:00:00', + due_date: '2023-02-10 00:00:00', + }, + { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2050-06-24 00:00:00', + due_date: '2050-06-23 00:00:00', + }, + { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06 00:00:00', + due_date: '2050-06-26 00:00:00', + }, + { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06 00:00:00', + due_date: '2050-06-24 00:00:00', + }, +]; + +const assessmentResponsesRows = [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: null as number, + response: null as string, + score: null as number, + grader_response: null as string, + }, + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + response: null as string, + score: null as number, + grader_response: null as string, + }, + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + response: null as string, + score: null as number, + grader_response: null as string, + }, + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + response: null as string, + score: 1, + grader_response: 'Well done!', + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: null as number, + response: null as string, + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: null as number, + response: '
Hello world!
', + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: null as number, + response: '
Hello world!
', + score: 0, + grader_response: 'Very close!', + }, + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + score: null as number, + grader_response: 'Well done!', + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + response: '
Hello world!
', + score: 0, + grader_response: 'Very close!', + }, +]; + +/* EXAMPLE DATA: Structured Data */ + +const curriculumAssessments: CurriculumAssessment[] = [ + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + id: 28, + question_id: 24, + description: null, + title: 'A relational database management system', + sort_order: 1, + }, + ], + }, + ], + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 3, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + id: 28, + question_id: 24, + description: null, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + correct_answer_id: 28, + }, + ], + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + id: 28, + question_id: 24, + description: null, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + correct_answer_id: 28, + }, + ], + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + id: 24, + assessment_id: 8, + title: + 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?', + description: null, + question_type: 'free response', + answers: [ + { + id: 29, + question_id: 24, + description: null, + title: '

Hello, World!

', + sort_order: 1, + correct_answer: true, + }, + ], + correct_answer_id: 29, + max_score: 1, + sort_order: 1, + }, + ], + }, + { + title: 'New Curriculum Quiz', + assessment_type: 'quiz', + description: null as string, + max_score: 42, + max_num_submissions: 13, + time_limit: 60, + curriculum_id: 4, + activity_id: 200, + principal_id: 300, + }, + { + title: 'New Curriculum Quiz', + assessment_type: 'quiz', + description: null, + max_score: 42, + max_num_submissions: 13, + time_limit: 60, + curriculum_id: 4, + activity_id: 200, + principal_id: 300, + questions: [ + { + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + answers: [ + { + description: null, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + max_score: 1, + sort_order: 1, + }, + ], + }, + { + title: 'New Curriculum Quiz', + assessment_type: 'quiz', + description: null, + max_score: 42, + max_num_submissions: 13, + time_limit: 60, + curriculum_id: 4, + activity_id: 200, + principal_id: 300, + questions: [ + { + assessment_id: 8, + title: + 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?', + description: null, + question_type: 'free response', + answers: [ + { + description: null, + title: '

Hello, World!

', + sort_order: 1, + correct_answer: true, + }, + ], + max_score: 1, + sort_order: 1, + }, + ], + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 121, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + answers: [ + { + description: null, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + max_score: 1, + sort_order: 1, + }, + ], + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + answers: [ + { + description: null, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + max_score: 1, + sort_order: 1, + }, + ], + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + description: null, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + }, + ], + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + id: 28, + question_id: 24, + description: 'Also known as a DBMS.', + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + correct_answer_id: 28, + }, + ], + }, + { + title: 'New Curriculum Quiz', + assessment_type: 'quiz', + description: null as string, + max_score: 42, + max_num_submissions: 13, + time_limit: 60, + curriculum_id: 4, + activity_id: 200, + principal_id: 300, + id: 9, + }, + { + title: 'New Curriculum Quiz', + assessment_type: 'quiz', + description: null, + max_score: 42, + max_num_submissions: 13, + time_limit: 60, + curriculum_id: 4, + activity_id: 200, + principal_id: 300, + id: 9, + questions: [ + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + id: 28, + question_id: 24, + description: null, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + correct_answer_id: 28, + }, + ], + }, + { + title: 'New Curriculum Quiz', + assessment_type: 'quiz', + description: null, + max_score: 42, + max_num_submissions: 13, + time_limit: 60, + curriculum_id: 4, + activity_id: 200, + principal_id: 300, + id: 9, + questions: [ + { + id: 24, + assessment_id: 8, + title: + 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?', + description: null, + question_type: 'free response', + answers: [ + { + id: 29, + question_id: 24, + description: null, + title: '

Hello, World!

', + sort_order: 1, + correct_answer: true, + }, + ], + correct_answer_id: 29, + max_score: 1, + sort_order: 1, + }, + ], + }, +]; + +const programAssessments: ProgramAssessment[] = [ + { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06T00:00:00.000-08:00', + due_date: '2050-06-24T00:00:00.000-07:00', + }, + { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06T00:00:00.000-08:00', + due_date: '2023-02-10T00:00:00.000-08:00', + }, + { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2050-06-24T00:00:00.000-07:00', + due_date: '2050-06-23T00:00:00.000-07:00', + }, + { + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06 00:00:00', + due_date: '2050-06-24 00:00:00', + }, +]; + +const assessmentDetails: AssessmentDetails[] = [ + { + curriculum_assessment: { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + id: 28, + question_id: 24, + description: null, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + correct_answer_id: 28, + }, + ], + }, + program_assessment: { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06T00:00:00.000-08:00', + due_date: '2050-06-24T00:00:00.000-07:00', + }, + }, +]; + +const participantSummaries: ParticipantAssessmentSubmissionsSummary[] = [ + { + principal_id: 30, + highest_state: 'Inactive', + total_num_submissions: 0, + }, + { + principal_id: 30, + highest_state: 'Expired', + total_num_submissions: 1, + }, + { + principal_id: 30, + highest_state: 'Active', + total_num_submissions: 0, + }, + { + principal_id: 30, + highest_state: 'Graded', + most_recent_submitted_date: '2023-02-09T13:23:45.000Z', + total_num_submissions: 1, + highest_score: 4, + }, +]; + +const facilitatorSummaries: FacilitatorAssessmentSubmissionsSummary[] = [ + { + num_participants_with_submissions: 8, + num_program_participants: 12, + num_ungraded_submissions: 6, + }, +]; + +const assessmentSubmissions: AssessmentSubmission[] = [ + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Opened', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:00:00.000Z', + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Opened', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:00:00.000Z', + responses: [ + { id: 320, assessment_id: 16, submission_id: 32, question_id: 24 }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'In Progress', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:05:00.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'In Progress', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:05:00.000Z', + responses: [ + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'In Progress', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:05:00.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'In Progress', + opened_at: '2023-02-10T07:00:00.000Z', + last_modified: '2023-02-10T07:05:00.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Expired', + opened_at: '2023-02-10T07:00:00.000Z', + last_modified: '2023-02-17T08:00:10.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Expired', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T14:00:00.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Expired', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-16T14:00:10.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Submitted', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T13:23:45.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Submitted', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T13:23:45.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + responses: [ + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + }, + ], + }, + { + id: 36, + assessment_id: 16, + principal_id: 32, + assessment_submission_state: 'Submitted', + opened_at: '2023-02-09T12:01:00.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + last_modified: '2023-02-09T13:23:45.000Z', + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Graded', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T13:23:45.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + score: 4, + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Graded', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T13:23:45.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + score: 4, + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + score: 1, + grader_response: 'Well done!', + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Graded', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T13:23:45.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Opened', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:05:00.000Z', + responses: [ + { + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Opened', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:05:00.000Z', + responses: [ + { + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Submitted', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-10T13:23:45.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + responses: [ + { + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + id: 320, + score: null as number, + grader_response: null as string, + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Submitted', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-10T13:23:45.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + responses: [ + { + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + id: 321, + score: null as number, + grader_response: null as string, + }, + ], + }, + { + id: 33, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Submitted', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-10T13:23:45.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + responses: [ + { + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + id: 320, + score: null as number, + grader_response: null as string, + }, + ], + }, +]; + +const assessmentsWithSubmissions: AssessmentWithSubmissions[] = [ + { + curriculum_assessment: { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + }, + program_assessment: { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06T00:00:00.000-08:00', + due_date: '2050-06-24T00:00:00.000-07:00', + }, + principal_program_role: 'Participant', + submissions: [ + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'In Progress', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:05:00.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + ], + }, + ], + }, + { + curriculum_assessment: { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + }, + program_assessment: { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06T00:00:00.000-08:00', + due_date: '2050-06-24T00:00:00.000-07:00', + }, + principal_program_role: 'Facilitator', + submissions: [ + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'In Progress', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:05:00.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + ], + }, + { + id: 36, + assessment_id: 16, + principal_id: 32, + assessment_submission_state: 'Submitted', + opened_at: '2023-02-09T12:01:00.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + last_modified: '2023-02-09T13:23:45.000Z', + }, + ], + }, +]; + +const savedAssessments: SavedAssessment[] = [ + { + curriculum_assessment: { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + id: 28, + question_id: 24, + description: null, + title: 'A relational database management system', + sort_order: 1, + }, + ], + }, + ], + }, + program_assessment: { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06T00:00:00.000-08:00', + due_date: '2050-06-24T00:00:00.000-07:00', + }, + principal_program_role: 'Participant', + submission: { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Opened', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:00:00.000Z', + }, + }, + { + curriculum_assessment: { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 3, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + id: 28, + question_id: 24, + description: null, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + correct_answer_id: 28, + }, + ], + }, + program_assessment: { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06T00:00:00.000-08:00', + due_date: '2050-06-24T00:00:00.000-07:00', + }, + principal_program_role: 'Participant', + submission: { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Opened', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:00:00.000Z', + }, + }, +]; + +describe('assessmentsRouter', () => { + const appAgent = createAppAgentForRouter(assessmentsRouter); + + describe('GET /', () => { + it('should respond with an empty list for a user not enrolled in any programs', done => { + mockListPrincipalEnrolledProgramIds.mockResolvedValue([]); + + mockPrincipalId(unenrolledPrincipalId); + + appAgent.get('/').expect(200, collectionEnvelope([], 0), err => { + expect(mockListPrincipalEnrolledProgramIds).toHaveBeenCalledWith( + unenrolledPrincipalId + ); + done(err); + }); + }); + + it('should respond with a list of all assessments (without questions) for participant enrolled in one program', done => { + const ParticipantAssessmentSubmissionsSummary: AssessmentWithSummary[] = [ + { + curriculum_assessment: curriculumAssessments[0], + program_assessment: programAssessments[0], + participant_submissions_summary: participantSummaries[3], + principal_program_role: 'Participant', + }, + ]; + mockListPrincipalEnrolledProgramIds.mockResolvedValue([ + programAssessments[0].program_id, + ]); + mockGetPrincipalProgramRole.mockResolvedValue('Participant'); + mockListProgramAssessments.mockResolvedValue([programAssessments[0]]); + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[0]); + mockConstructParticipantAssessmentSummary.mockResolvedValue( + participantSummaries[3] + ); + mockPrincipalId(participantPrincipalId); + + appAgent + .get('/') + .expect( + 200, + collectionEnvelope( + ParticipantAssessmentSubmissionsSummary, + ParticipantAssessmentSubmissionsSummary.length + ), + err => { + expect(mockListPrincipalEnrolledProgramIds).toHaveBeenCalledWith( + participantPrincipalId + ); + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].program_id + ); + expect(mockListProgramAssessments).toHaveBeenCalledWith( + programAssessments[0].program_id + ); + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + programAssessments[0].assessment_id, + false, + false + ); + expect( + mockConstructParticipantAssessmentSummary + ).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0] + ); + done(err); + } + ); + }); + + it('should respond with a list of all assessments (without questions) for facilitator of one program', done => { + const facilitatorAssessmentListResponse: AssessmentWithSummary[] = [ + { + curriculum_assessment: curriculumAssessments[0], + program_assessment: programAssessments[0], + facilitator_submissions_summary: facilitatorSummaries[0], + principal_program_role: 'Facilitator', + }, + ]; + mockListPrincipalEnrolledProgramIds.mockResolvedValue([ + programAssessments[0].program_id, + ]); + mockGetPrincipalProgramRole.mockResolvedValue('Facilitator'); + mockListProgramAssessments.mockResolvedValue([programAssessments[0]]); + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[0]); + mockConstructFacilitatorAssessmentSummary.mockResolvedValue( + facilitatorSummaries[0] + ); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .get('/') + .expect( + 200, + collectionEnvelope( + facilitatorAssessmentListResponse, + facilitatorAssessmentListResponse.length + ), + err => { + expect(mockListPrincipalEnrolledProgramIds).toHaveBeenCalledWith( + facilitatorPrincipalId + ); + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + facilitatorPrincipalId, + programAssessments[0].program_id + ); + expect(mockListProgramAssessments).toBeCalledWith( + programAssessments[0].program_id + ); + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + programAssessments[0].assessment_id, + false, + false + ); + expect( + mockConstructFacilitatorAssessmentSummary + ).toHaveBeenCalledWith(programAssessments[0]); + done(err); + } + ); + }); + + it('should throw an error if a database error was encountered', done => { + mockListPrincipalEnrolledProgramIds.mockRejectedValue(new Error()); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent.get('/').expect(500, done); + }); + }); + + describe('GET /demo/facilitator', () => { + it('should enroll a user as a facilitator in a program, then redirect to assessments list page', done => { + mockEnrollFacilitator.mockResolvedValue(true); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent.get(`/demo/facilitator`).expect(302, done); + }); + + it('should do nothing if the user is already a facilitator', done => { + mockEnrollFacilitator.mockResolvedValue(false); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent.get(`/demo/facilitator`).expect(204, done); + }); + + it('should throw an error if a database error was encountered', done => { + mockEnrollFacilitator.mockRejectedValue(new Error()); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent.get(`/demo/facilitator`).expect(500, done); + }); + }); + + describe('GET /demo/participant', () => { + it('should enroll a user as a participant in a program, then redirect to assessments list page', done => { + mockEnrollParticipant.mockResolvedValue(true); + + mockPrincipalId(participantPrincipalId); + + appAgent.get(`/demo/participant`).expect(302, done); + }); + + it('should do nothing if the user is already a participant', done => { + mockEnrollParticipant.mockResolvedValue(false); + + mockPrincipalId(participantPrincipalId); + + appAgent.get(`/demo/participant`).expect(204, done); + }); + + it('should throw an error if a database error was encountered', done => { + mockEnrollParticipant.mockRejectedValue(new Error()); + + mockPrincipalId(participantPrincipalId); + + appAgent.get(`/demo/participant`).expect(500, done); + }); + }); + + describe('GET /curriculum/:curriculumAssessmentId', () => { + it('should retrieve a curriculum assessment if the logged-in principal ID is the program facilitator', done => { + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[3]); + mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue([ + programAssessments[0].program_id, + ]); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .get(`/curriculum/${curriculumAssessments[3].id}`) + .expect(200, itemEnvelope(curriculumAssessments[3]), err => { + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + curriculumAssessments[3].id, + true, + true + ); + expect( + mockFacilitatorProgramIdsMatchingCurriculum + ).toHaveBeenCalledWith( + facilitatorPrincipalId, + curriculumAssessments[3].curriculum_id + ); + + done(err); + }); + }); + + it('should respond with an UnauthorizedError if the logged-in principal ID is not the program facilitator', done => { + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[3]); + mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue([]); + + mockPrincipalId(participantPrincipalId); + + appAgent + .get(`/curriculum/${curriculumAssessments[3].id}`) + .expect( + 401, + errorEnvelope( + `Not allowed to access curriculum assessment with ID ${curriculumAssessments[3].id}.` + ), + err => { + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + curriculumAssessments[3].id, + true, + true + ); + expect( + mockFacilitatorProgramIdsMatchingCurriculum + ).toHaveBeenCalledWith( + participantPrincipalId, + curriculumAssessments[3].curriculum_id + ); + + done(err); + } + ); + }); + + it('should respond with an BadRequestError if the curriculum assessment ID is not a number', done => { + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .get(`/curriculum/test`) + .expect( + 400, + errorEnvelope(`"${Number(test)}" is not a valid submission ID.`), + err => { + done(err); + } + ); + }); + + it('should respond with a NotFoundError if the curriculum assessment ID was not found in the database', done => { + mockGetCurriculumAssessment.mockResolvedValue(null); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .get(`/curriculum/${curriculumAssessments[3].id}`) + .expect( + 404, + errorEnvelope( + `Could not find curriculum assessment with ID ${curriculumAssessments[3].id}.` + ), + err => { + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + curriculumAssessments[3].id, + true, + true + ); + + done(err); + } + ); + }); + }); + + describe('POST /curriculum', () => { + it('should create a curriculum assessment if the logged-in principal ID is the program facilitator', done => { + const matchingFacilitatorPrograms = [3, 4, 6]; + mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue( + matchingFacilitatorPrograms + ); + mockCreateCurriculumAssessment.mockResolvedValue( + curriculumAssessments[13] + ); + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .post(`/curriculum`) + .send(curriculumAssessments[5]) + .expect(201, err => { + expect( + mockFacilitatorProgramIdsMatchingCurriculum + ).toHaveBeenCalledWith( + facilitatorPrincipalId, + curriculumAssessments[5].curriculum_id + ); + expect(mockCreateCurriculumAssessment).toHaveBeenCalledWith( + curriculumAssessments[5] + ); + + done(err); + }); + }); + + it('should respond with an Unauthorized Error if the logged-in principal ID is not the facilitator', done => { + mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue([]); + mockPrincipalId(participantPrincipalId); + + appAgent + .post(`/curriculum`) + .send(curriculumAssessments[5]) + .expect( + 401, + errorEnvelope( + `Not allowed to add a new assessment for this curriculum.` + ), + err => { + expect( + mockFacilitatorProgramIdsMatchingCurriculum + ).toHaveBeenCalledWith( + participantPrincipalId, + curriculumAssessments[5].curriculum_id + ); + done(err); + } + ); + }); + + it('should reponse with BadRequestError if the information missing', done => { + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .post(`/curriculum`) + .send({ description: 'test' }) + .expect( + 422, + errorEnvelope(`Was not given a valid curriculum assessment.`), + err => { + done(err); + } + ); + }); + }); + + describe('PUT /curriculum/:curriculumAssessmentId', () => { + it('should update a curriculum assessment if the logged-in principal ID is the program facilitator', done => { + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[3]); + mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue( + facilitatorProgramIdsThatMatchCurriculum + ); + mockUpdateCurriculumAssessment.mockResolvedValue( + curriculumAssessments[8] + ); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .put(`/curriculum/${curriculumAssessments[0].id}`) + .send(curriculumAssessments[8]) + .expect(200, itemEnvelope(curriculumAssessments[8]), err => { + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + curriculumAssessments[0].id + ); + expect( + mockFacilitatorProgramIdsMatchingCurriculum + ).toHaveBeenCalledWith(facilitatorPrincipalId, curriculumId); + expect(mockUpdateCurriculumAssessment).toHaveBeenCalledWith( + curriculumAssessments[8] + ); + + done(err); + }); + }); + + it('should respond with a BadRequestError if the curriculumAssessment ID is not a valid number', done => { + const curriculumAssessmentIdInvalid = 0; + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .put(`/curriculum/${curriculumAssessmentIdInvalid}`) + .expect( + 400, + errorEnvelope( + `"${Number( + curriculumAssessmentIdInvalid + )}" is not a valid curriculum assessment ID.` + ), + done + ); + }); + + it('should respond with a ValidationError if given an curriculum assessment is not valid', done => { + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .put(`/curriculum/${curriculumAssessments[0].id}`) + .send({ assessment_type: 'test' }) + .expect( + 422, + errorEnvelope(`Was not given a valid curriculum assessment.`), + err => { + done(err); + } + ); + }); + + it('should respond with a NotFoundError if the curriculum assessment ID was not found in the database', done => { + mockGetCurriculumAssessment.mockResolvedValue(null); + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .put(`/curriculum/${curriculumAssessments[0].id}`) + .send(curriculumAssessments[8]) + .expect( + 404, + errorEnvelope( + `Could not find curriculum assessment with ID ${curriculumAssessments[0].id}.` + ), + err => { + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + curriculumAssessments[0].id + ); + + done(err); + } + ); + }); + + it('should respond with an Unauthorized Error if the the facilitator is not taking the program with the curriculum', done => { + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[0]); + mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue([]); + + mockPrincipalId(participantPrincipalId); + + appAgent + .put(`/curriculum/${curriculumAssessments[0].id}`) + .send(curriculumAssessments[8]) + .expect( + 401, + errorEnvelope( + `Not allowed to make modifications to curriculum assessment with ID ${curriculumAssessments[0].id}.` + ), + err => { + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + curriculumAssessments[0].id + ); + + expect( + mockFacilitatorProgramIdsMatchingCurriculum + ).toHaveBeenCalledWith(participantPrincipalId, curriculumId); + + done(err); + } + ); + }); + + it('should respond with InternalServerError if curriculum assessment with given ID could not be updated', done => { + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[3]); + mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue( + facilitatorProgramIdsThatMatchCurriculum + ); + mockUpdateCurriculumAssessment.mockResolvedValue(null); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .put(`/curriculum/${curriculumAssessments[0].id}`) + .send(curriculumAssessments[8]) + + .expect( + 500, + errorEnvelope( + `Could not update curriculum assessment with ID ${curriculumAssessments[0].id}.` + ), + err => { + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + curriculumAssessments[0].id + ); + expect( + mockFacilitatorProgramIdsMatchingCurriculum + ).toHaveBeenCalledWith(facilitatorPrincipalId, curriculumId); + expect(mockUpdateCurriculumAssessment).toHaveBeenCalledWith( + curriculumAssessments[8] + ); + done(err); + } + ); + }); + }); + + describe('DELETE /curriculum/:curriculumAssessmentId', () => { + it('should delete a curriculumAssessment if principal ID is a program facilitator of that curriculum', done => { + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[0]); + + mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue( + facilitatorProgramIdsThatMatchCurriculum + ); + mockDeleteCurriculumAssessment.mockResolvedValue(null); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .delete(`/curriculum/${curriculumAssessmentId}`) + .expect(204, {}, err => { + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + curriculumAssessmentId + ); + expect( + mockFacilitatorProgramIdsMatchingCurriculum + ).toHaveBeenCalledWith(facilitatorPrincipalId, curriculumId); + + expect(mockDeleteCurriculumAssessment).toHaveBeenCalledWith( + curriculumAssessments[0].id + ); + + done(err); + }); + }); + + it('should respond with an Unauthorized Error if the the facilitator is not taking the program with the curriculum', done => { + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[0]); + mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue([]); + + mockPrincipalId(participantPrincipalId); + + appAgent + .delete(`/curriculum/${curriculumAssessmentId}`) + .expect( + 401, + errorEnvelope( + `Not allowed to delete curriculum assessment with ID ${curriculumAssessmentId}.` + ), + err => { + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + curriculumAssessmentId + ); + + expect( + mockFacilitatorProgramIdsMatchingCurriculum + ).toHaveBeenCalledWith(participantPrincipalId, curriculumId); + + done(err); + } + ); + }); + + it('should respond with a BadRequestError if the curriculumAssessment ID is not a valid number', done => { + const curriculumId = 'test'; + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .delete(`/curriculum/${curriculumId}`) + .expect( + 400, + errorEnvelope( + `"${Number(curriculumId)}" is not a valid curriculum assessment ID.` + ), + done + ); + }); + + it('should respond with a NotFoundError if the curriculum assessment ID was not found in the database', done => { + mockGetCurriculumAssessment.mockResolvedValue(null); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .delete(`/curriculum/${curriculumAssessmentId}`) + .expect( + 404, + errorEnvelope( + `Could not find curriculum assessment with ID ${curriculumAssessmentId}.` + ), + err => { + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + curriculumAssessmentId + ); + + done(err); + } + ); + }); + + it('should respond with a ConflictError if trying to delete curriculum Assessment ID that has participant submissions', done => { + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[0]); + mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue( + facilitatorProgramIdsThatMatchCurriculum + ); + mockDeleteCurriculumAssessment.mockRejectedValue(new Error()); + + mockPrincipalId(facilitatorPrincipalId); + appAgent + .delete(`/curriculum/${curriculumAssessmentId}`) + .expect( + 409, + errorEnvelope(`Cannot delete a curriculum assessment.`), + err => { + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + curriculumAssessmentId + ); + + expect( + mockFacilitatorProgramIdsMatchingCurriculum + ).toHaveBeenCalledWith(facilitatorPrincipalId, curriculumId); + + expect(mockDeleteCurriculumAssessment).toHaveBeenCalledWith( + curriculumAssessments[0].id + ); + + done(err); + } + ); + }); + }); + + describe('GET /program/:programAssessmentId', () => { + it('should get a program assessment if the logged-in principal ID is the program facilitator', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Facilitator'); + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[3]); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .get(`/program/${programAssessments[0].id}`) + .expect(200, itemEnvelope(assessmentDetails[0]), err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + facilitatorPrincipalId, + programAssessments[0].program_id + ); + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + programAssessments[0].assessment_id, + true, + true + ); + + done(err); + }); + }); + + it('should respond with an Unauthorized Error if the logged-in principal ID is not the facilitator', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue(null); + + mockPrincipalId(otherParticipantPrincipalId); + + appAgent + .get(`/program/${programAssessments[0].id}`) + .expect( + 401, + errorEnvelope( + `Could not access assessment with Program Assessment ID ${programAssessments[0].id}.` + ), + err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + otherParticipantPrincipalId, + programAssessments[0].program_id + ); + + done(err); + } + ); + }); + + it('should respond with an BadRequestError if the program assessment ID is not a number', done => { + const exampleAssessmentFromUser = 'test'; + + mockPrincipalId(otherParticipantPrincipalId); + + appAgent + .get(`/program/${exampleAssessmentFromUser}`) + .expect( + 400, + errorEnvelope( + `"${Number( + exampleAssessmentFromUser + )}" is not a valid submission ID.` + ), + done + ); + }); + + it('should respond with a NotFoundError if the program assessment ID was not found in the database', done => { + const programAssessmentId = 20; + + mockFindProgramAssessment.mockResolvedValue(null); + + mockPrincipalId(facilitatorPrincipalId); + appAgent + .get(`/program/${programAssessmentId}`) + .expect( + 404, + errorEnvelope( + `Could not find program assessment with ID ${programAssessmentId}.` + ), + err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessmentId + ); + + done(err); + } + ); + }); + }); + + describe('POST /program', () => { + it('should create a new program assessment if the logged-in principal ID is the program facilitator', done => { + mockGetPrincipalProgramRole.mockResolvedValue('Facilitator'); + mockCreateProgramAssessment.mockResolvedValue(programAssessmentsRows[3]); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .post(`/program`) + .send(programAssessmentsRows[0]) + .expect(201, err => { + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + facilitatorPrincipalId, + programAssessmentsRows[0].program_id + ); + + expect(mockCreateProgramAssessment).toHaveBeenCalledWith( + programAssessmentsRows[0] + ); + + done(err); + }); + }); + + it('should respond with an Unauthorized Error if the logged-in principal ID is not the facilitator', done => { + mockGetPrincipalProgramRole.mockResolvedValue('Participant'); + + mockPrincipalId(participantPrincipalId); + + appAgent + .post(`/program`) + .send(programAssessmentsRows[0]) + .expect( + 401, + errorEnvelope( + `User is not allowed to create new program assessments for this program.` + ), + err => { + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + participantPrincipalId, + programAssessmentsRows[0].program_id + ); + + done(err); + } + ); + }); + + it('should respond with a BadRequestError if given an invalid program assessment', done => { + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .post(`/program`) + .send({ available_after: '2023-08-10' }) + .expect( + 400, + errorEnvelope(`Was not given a valid program assessment.`), + err => { + done(err); + } + ); + }); + }); + + describe('PUT /program/:programAssessmentId', () => { + it('should update a program assessment if the logged-in principal ID is the program facilitator', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Facilitator'); + mockUpdateProgramAssessment.mockResolvedValue(programAssessmentsRows[3]); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .put(`/program/${programAssessments[0].id}`) + .send(programAssessmentsRows[3]) + .expect(200, itemEnvelope(programAssessmentsRows[3]), err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + facilitatorPrincipalId, + programAssessments[0].program_id + ); + + expect(mockUpdateProgramAssessment).toHaveBeenCalledWith( + programAssessmentsRows[3] + ); + + done(err); + }); + }); + + it('should respond with an Unauthorized Error if the logged-in principal ID is not the facilitator', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue(null); + + mockPrincipalId(otherParticipantPrincipalId); + + appAgent + .put(`/program/${programAssessments[0].id}`) + .send(programAssessmentsRows[3]) + .expect( + 401, + errorEnvelope( + `Could not access program Assessment with ID ${programAssessments[0].id}.` + ), + err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + otherParticipantPrincipalId, + programAssessments[0].program_id + ); + + done(err); + } + ); + }); + + it('should respond with an BadRequestError if the program assessment ID is not a number', done => { + const exampleAssessmentFromUser = 'test'; + + mockPrincipalId(otherParticipantPrincipalId); + + appAgent + .put(`/program/${exampleAssessmentFromUser}`) + .send(programAssessments[0]) + .expect( + 400, + errorEnvelope( + `"${Number( + exampleAssessmentFromUser + )}" is not a valid program assessment ID.` + ), + done + ); + }); + + it('should respond with an BadRequestError if not given a valid program assessment', done => { + const exampleAssessmentFormUser = 'test'; + + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Facilitator'); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .put(`/program/${programAssessments[0].id}`) + .send(exampleAssessmentFormUser) + .expect( + 400, + errorEnvelope(`Was not given a valid program assessment.`), + err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + facilitatorPrincipalId, + programAssessments[0].program_id + ); + + done(err); + } + ); + }); + + it('should respond with a NotFoundError if the program assessment ID was not found in the database', done => { + const programAssessmentId = 20; + + mockFindProgramAssessment.mockResolvedValue(null); + + mockPrincipalId(facilitatorPrincipalId); + appAgent + .put(`/program/${programAssessmentId}`) + .send(programAssessmentsRows[3]) + .expect( + 404, + errorEnvelope( + `Could not find program assessment with ID ${programAssessmentId}.` + ), + err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessmentId + ); + + done(err); + } + ); + }); + }); + + describe('DELETE /program/:programAssessmentId', () => { + it('should delete a program assessment in the system if logged-in user is facilitator of that program', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Facilitator'); + mockDeleteProgramAssessment.mockResolvedValue(null); + + mockPrincipalId(facilitatorPrincipalId); + appAgent + .delete(`/program/${programAssessments[0].id}`) + .expect(204, {}, err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + facilitatorPrincipalId, + programAssessments[0].program_id + ); + expect(mockDeleteProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + done(err); + }); + }); + + it('should return an error if logged-in user is not a facilitator of that program', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Participant'); + + mockPrincipalId(participantPrincipalId); + appAgent + .delete(`/program/${programAssessments[0].id}`) + .expect( + 401, + errorEnvelope( + `Not allowed to access program assessment with ID ${programAssessments[0].id}.` + ), + err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].program_id + ); + done(err); + } + ); + }); + + it('should respond with a BadRequestError if given an invalid program assessment ID', done => { + const programAssessmentId = 'test'; + + appAgent + .delete(`/program/${programAssessmentId}`) + .expect( + 400, + errorEnvelope( + `"${Number( + programAssessmentId + )}" is not a valid program assessment ID.` + ), + done + ); + }); + it('should respond with a NotFoundError if assessment ID not exist', done => { + mockFindProgramAssessment.mockResolvedValue(null); + appAgent + .delete(`/program/${programAssessments[0].id}`) + .expect( + 404, + errorEnvelope( + `Could not find program assessment with ID ${programAssessments[0].id}.` + ), + err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + done(err); + } + ); + }); + it('should respond with a ConflictError if trying to delete program assessment that has participant submissions', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Facilitator'); + mockDeleteProgramAssessment.mockRejectedValue(new Error()); + mockPrincipalId(facilitatorPrincipalId); + appAgent + .delete(`/program/${programAssessments[0].id}`) + .expect( + 409, + errorEnvelope( + `Cannot delete a program assessment that has participant submissions.` + ), + err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + facilitatorPrincipalId, + programAssessments[0].program_id + ); + expect(mockDeleteProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + done(err); + } + ); + }); + }); + + describe('GET /program/:programAssessmentId/submissions', () => { + it('should show a facilitator an AssessmentWithSubmissions with all participant submissions', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Facilitator'); + mockListAllProgramAssessmentSubmissions.mockResolvedValue([ + assessmentSubmissions[2], + assessmentSubmissions[11], + ]); + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[0]); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .get(`/program/${programAssessments[0].id}/submissions`) + .expect(200, itemEnvelope(assessmentsWithSubmissions[1]), err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + facilitatorPrincipalId, + programAssessments[0].program_id + ); + + expect(mockListAllProgramAssessmentSubmissions).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + programAssessments[0].assessment_id, + false, + false + ); + + done(err); + }); + }); + + it('should show a participant an AssessmentWithSubmissions with their submissions', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Participant'); + mockListParticipantProgramAssessmentSubmissions.mockResolvedValue([ + assessmentSubmissions[2], + ]); + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[0]); + + mockPrincipalId(participantPrincipalId); + appAgent + .get(`/program/${programAssessments[0].id}/submissions`) + .expect(200, itemEnvelope(assessmentsWithSubmissions[0]), err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].program_id + ); + + expect( + mockListParticipantProgramAssessmentSubmissions + ).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].id + ); + + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + programAssessments[0].assessment_id, + false, + false + ); + + done(err); + }); + }); + + it('should give an UnauthorizedError to anyone not enrolled in the course', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue(null); + + mockPrincipalId(unenrolledPrincipalId); + appAgent + .get(`/program/${programAssessments[0].id}/submissions`) + .expect( + 401, + errorEnvelope( + `Could not access program assessment with ID ${programAssessments[0].id} without enrollment.` + ), + err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + unenrolledPrincipalId, + programAssessments[0].program_id + ); + + done(err); + } + ); + }); + + it('should give a NotFoundError for a programAssessmentId not found in the database', done => { + mockFindProgramAssessment.mockResolvedValue(null); + + mockPrincipalId(participantPrincipalId); + appAgent + .get(`/program/${programAssessments[0].id}/submissions`) + .expect( + 404, + errorEnvelope( + `Could not find program assessment with ID ${programAssessments[0].id}` + ), + err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + done(err); + } + ); + }); + + it('should give a BadRequestError for an invalid programAssessmentId', done => { + mockPrincipalId(participantPrincipalId); + appAgent + .get(`/program/test/submissions`) + .expect( + 400, + errorEnvelope(`"test" is not a valid program assessment ID.`), + err => { + done(err); + } + ); + }); + }); + + describe('GET /program/:programAssessmentId/submissions/new', () => { + it('should respond with a bad request error if given an invalid assessment id', done => { + const programAssessmentInvalidId = 'test'; + appAgent + .get(`/program/${programAssessmentInvalidId}/submissions/new`) + .expect( + 400, + errorEnvelope( + `"${programAssessmentInvalidId}" is not a valid program assessment ID.` + ), + done + ); + }); + + it('should respond with a NotFoundError if the assessment submission ID was not found in the database', done => { + mockFindProgramAssessment.mockResolvedValue(null); + + mockPrincipalId(participantPrincipalId); + + appAgent + .get(`/program/${programAssessments[0].id}/submissions/new`) + .expect( + 404, + errorEnvelope( + `Could not find program assessment with ID ${programAssessments[0].id}.` + ), + err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + done(err); + } + ); + }); + + it('should return an error when attempting to create a submission for a program assessment that is not yet available', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[2]); + + mockPrincipalId(participantPrincipalId); + + appAgent + .get(`/program/${programAssessments[2].id}/submissions/new`) + .expect( + 403, + errorEnvelope( + `Could not create a new submission of an assessment that's not yet available.` + ), + err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[2].id + ); + done(err); + } + ); + }); + + it('should return an error when attempting to create a submission when the program assessment due date has passed', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[1]); + + mockPrincipalId(participantPrincipalId); + + appAgent + .get(`/program/${programAssessments[1].id}/submissions/new`) + .expect( + 403, + errorEnvelope( + `Could not create a new submission of an assessment after its due date.` + ), + err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[1].id + ); + done(err); + } + ); + }); + + it('should return an error if logged-in user is not enrolled in the program', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue(null); + + mockPrincipalId(unenrolledPrincipalId); + + appAgent + .get(`/program/${programAssessments[0].id}/submissions/new`) + .expect( + 401, + errorEnvelope( + `Could not access program assessment with ID ${programAssessments[0].id}) without enrollment.` + ), + err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + unenrolledPrincipalId, + programAssessments[0].program_id + ); + + done(err); + } + ); + }); + + it('should return an error if logged-in user is a facilitator', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Facilitator'); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .get(`/program/${programAssessments[0].id}/submissions/new`) + .expect( + 401, + errorEnvelope( + `Facilitators are not allowed to create program assessment submissions.` + ), + err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + facilitatorPrincipalId, + programAssessments[0].program_id + ); + + done(err); + } + ); + }); + + it('should return an error if no possible submissions remain for this participant and this assessment', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Participant'); + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[1]); + mockListParticipantProgramAssessmentSubmissions.mockResolvedValue([ + assessmentSubmissions[11], + ]); + + mockPrincipalId(otherParticipantPrincipalId); + + appAgent + .get(`/program/${programAssessments[0].id}/submissions/new`) + .expect( + 403, + errorEnvelope( + `Could not create a new submission as you have reached the maximum number of submissions for this assessment.` + ), + err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + otherParticipantPrincipalId, + programAssessments[0].program_id + ); + + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + programAssessments[0].assessment_id, + true, + false + ); + + expect( + mockListParticipantProgramAssessmentSubmissions + ).toHaveBeenCalledWith( + otherParticipantPrincipalId, + programAssessments[0].id + ); + + done(err); + } + ); + }); + + it('should return the existing submission if one is currently opened or in progress', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Participant'); + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[1]); + mockListParticipantProgramAssessmentSubmissions.mockResolvedValue([ + assessmentSubmissions[0], + ]); + mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[0]); + + mockPrincipalId(participantPrincipalId); + + appAgent + .get(`/program/${programAssessments[0].id}/submissions/new`) + .expect(200, itemEnvelope(savedAssessments[0]), err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].program_id + ); + + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + programAssessments[0].assessment_id, + true, + false + ); + + expect( + mockListParticipantProgramAssessmentSubmissions + ).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].id + ); + + expect(mockGetAssessmentSubmission).toHaveBeenCalledWith( + assessmentSubmissions[2].id, + true, + false + ); + + done(err); + }); + }); + + it('should return a participant a new submission without including the correct answers', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Participant'); + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[1]); + mockListParticipantProgramAssessmentSubmissions.mockResolvedValue(null); + mockCreateAssessmentSubmission.mockResolvedValue( + assessmentSubmissions[0] + ); + + mockPrincipalId(participantPrincipalId); + + appAgent + .get(`/program/${programAssessments[0].id}/submissions/new`) + .expect(201, itemEnvelope(savedAssessments[0]), err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].program_id + ); + + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + programAssessments[0].assessment_id, + true, + false + ); + + expect( + mockListParticipantProgramAssessmentSubmissions + ).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].id + ); + + expect(mockCreateAssessmentSubmission).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].id, + programAssessments[0].assessment_id + ); + + done(err); + }); + }); + + it('should return a participant a new submission without including the correct answers, even if a prior submitted submission exists', done => { + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Participant'); + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[2]); + mockListParticipantProgramAssessmentSubmissions.mockResolvedValue([ + assessmentSubmissions[9], + ]); + mockCreateAssessmentSubmission.mockResolvedValue( + assessmentSubmissions[0] + ); + + mockPrincipalId(participantPrincipalId); + + appAgent + .get(`/program/${programAssessments[0].id}/submissions/new`) + .expect(201, itemEnvelope(savedAssessments[1]), err => { + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].program_id + ); + + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + programAssessments[0].assessment_id, + true, + false + ); + + expect( + mockListParticipantProgramAssessmentSubmissions + ).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].id + ); + + expect(mockCreateAssessmentSubmission).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].id, + programAssessments[0].assessment_id + ); + + done(err); + }); + }); + }); + + describe('GET /submissions/:submissionId', () => { + it("should show a facilitator the full submission information for a participant's ungraded submitted assessment, including the correct answers", done => { + const facilitatorFullResponse: SavedAssessment = { + curriculum_assessment: curriculumAssessments[3], + program_assessment: programAssessments[0], + principal_program_role: 'Facilitator', + submission: assessmentSubmissions[9], + }; + + mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[9]); + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Facilitator'); + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[3]); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .get(`/submissions/${assessmentSubmissions[9].id}`) + .expect(200, itemEnvelope(facilitatorFullResponse), err => { + expect(mockGetAssessmentSubmission).toHaveBeenCalledWith( + assessmentSubmissions[9].id, + true + ); + + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + facilitatorPrincipalId, + programAssessments[0].program_id + ); + + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + programAssessments[0].assessment_id, + true, + true + ); + + done(err); + }); + }); + + it('should show a participant their submission information for an in-progress assessment without including the correct answers', done => { + const participantInProgressAssessmentSubmission: SavedAssessment = { + curriculum_assessment: curriculumAssessments[1], + program_assessment: programAssessments[0], + principal_program_role: 'Participant', + submission: assessmentSubmissions[2], + }; + + mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[2]); + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Participant'); + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[1]); + + mockPrincipalId(participantPrincipalId); + + appAgent + .get(`/submissions/${assessmentSubmissions[2].id}`) + .expect( + 200, + itemEnvelope(participantInProgressAssessmentSubmission), + err => { + expect(mockGetAssessmentSubmission).toHaveBeenCalledWith( + assessmentSubmissions[2].id, + true + ); + + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].program_id + ); + + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + programAssessments[0].assessment_id, + true, + false + ); + + done(err); + } + ); + }); + + it('should show a participant their submission information for an ungraded submitted assessment without including the correct answers', done => { + const participantSubmittedAssessmentSubmission: SavedAssessment = { + curriculum_assessment: curriculumAssessments[1], + program_assessment: programAssessments[0], + principal_program_role: 'Participant', + submission: assessmentSubmissions[9], + }; + + mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[9]); + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Participant'); + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[1]); + + mockPrincipalId(participantPrincipalId); + + appAgent + .get(`/submissions/${assessmentSubmissions[9].id}`) + .expect( + 200, + itemEnvelope(participantSubmittedAssessmentSubmission), + err => { + expect(mockGetAssessmentSubmission).toHaveBeenCalledWith( + assessmentSubmissions[9].id, + true + ); + + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].program_id + ); + + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + programAssessments[0].assessment_id, + true, + false + ); + + done(err); + } + ); + }); + + it('should show a participant their submission information for a graded submitted assessment including the correct answers', done => { + const participantGradedAssessmentSubmission: SavedAssessment = { + curriculum_assessment: curriculumAssessments[3], + program_assessment: programAssessments[0], + principal_program_role: 'Participant', + submission: assessmentSubmissions[13], + }; + + mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[13]); + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Participant'); + mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[3]); + + mockPrincipalId(participantPrincipalId); + + appAgent + .get(`/submissions/${assessmentSubmissions[13].id}`) + .expect( + 200, + itemEnvelope(participantGradedAssessmentSubmission), + err => { + expect(mockGetAssessmentSubmission).toHaveBeenCalledWith( + assessmentSubmissions[13].id, + true + ); + + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].program_id + ); + + expect(mockGetCurriculumAssessment).toHaveBeenCalledWith( + programAssessments[0].assessment_id, + true, + true + ); + + done(err); + } + ); + }); + + it('should respond with a BadRequestError if given an invalid submission ID', done => { + const submissionId = 'test'; + + appAgent + .get(`/submissions/${submissionId}`) + .expect( + 400, + errorEnvelope( + `"${Number(submissionId)}" is not a valid submission ID.` + ), + done + ); + }); + + it('should respond with a NotFoundError if the submission ID was not found in the database', done => { + const submissionId = 8; + mockGetAssessmentSubmission.mockResolvedValue(null); + + mockPrincipalId(participantPrincipalId); + + appAgent + .get(`/submissions/${submissionId}`) + .expect( + 404, + errorEnvelope(`Could not find submission with ID ${submissionId}.`), + err => { + expect(mockGetAssessmentSubmission).toHaveBeenCalledWith( + submissionId, + true + ); + + done(err); + } + ); + }); + + it('should respond with an Unauthorized Error if the logged-in principal ID is not the same as the principal ID of the submission ID and is not the principal ID of the program facilitator', done => { + mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[9]); + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Participant'); + + mockPrincipalId(otherParticipantPrincipalId); + + appAgent + .get(`/submissions/${assessmentSubmissions[9].id}`) + .expect( + 401, + errorEnvelope( + `Could not access submission with ID ${assessmentSubmissions[9].id}.` + ), + err => { + expect(mockGetAssessmentSubmission).toHaveBeenCalledWith( + assessmentSubmissions[9].id, + true + ); + + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + otherParticipantPrincipalId, + programAssessments[0].program_id + ); + + done(err); + } + ); + }); + + it('should respond with an Unauthorized Error if logged-in principal ID is not enrolled in the program', done => { + const programId = 12; + mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[9]); + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue(null); + + mockPrincipalId(participantPrincipalId); + + appAgent + .get(`/submissions/${assessmentSubmissions[9].id}`) + .expect( + 401, + errorEnvelope( + `Could not access submission with ID ${assessmentSubmissions[9].id}.` + ), + err => { + expect(mockGetAssessmentSubmission).toHaveBeenCalledWith( + assessmentSubmissions[9].id, + true + ); + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + participantPrincipalId, + programId + ); + done(err); + } + ); + }); + + it('should respond with an internal server error if a database error occurs', done => { + const submissionId = 10; + mockGetAssessmentSubmission.mockRejectedValue(new Error()); + appAgent.get(`/submissions/${submissionId}`).expect(500, done); + }); + }); + + describe('PUT /submissions/:submissionId', () => { + it('should update submission if the logged-in principal ID is the program facilitator', done => { + mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[9]); + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Facilitator'); + mockUpdateAssessmentSubmission.mockResolvedValue( + assessmentSubmissions[17] + ); + + mockPrincipalId(facilitatorPrincipalId); + + appAgent + .put(`/submissions/${assessmentSubmissions[9].id}`) + .send(assessmentSubmissions[17]) + .expect(200, itemEnvelope(assessmentSubmissions[17]), err => { + expect(mockGetAssessmentSubmission).toHaveBeenCalledWith( + assessmentSubmissions[9].id, + true + ); + + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + assessmentSubmissions[17].assessment_id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + facilitatorPrincipalId, + programAssessments[0].program_id + ); + + expect(mockUpdateAssessmentSubmission).toHaveBeenCalledWith( + assessmentSubmissions[17], + true + ); + + done(err); + }); + }); + + it('should update submission if the logged-in principal ID is the program participant', done => { + mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[2]); + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Participant'); + mockUpdateAssessmentSubmission.mockResolvedValue( + assessmentSubmissions[2] + ); + + mockPrincipalId(participantPrincipalId); + + appAgent + .put(`/submissions/${assessmentSubmissions[2].id}`) + .send(assessmentSubmissions[2]) + .expect(200, itemEnvelope(assessmentSubmissions[2]), err => { + expect(mockGetAssessmentSubmission).toHaveBeenCalledWith( + assessmentSubmissions[2].id, + true + ); + + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + assessmentSubmissions[2].assessment_id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].program_id + ); + + expect(mockUpdateAssessmentSubmission).toHaveBeenCalledWith( + assessmentSubmissions[2], + false + ); + + done(err); + }); + }); + + it('should do nothing if the logged-in principal ID is the program participant and the assessment was already submitted', done => { + mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[9]); + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Participant'); + + mockPrincipalId(participantPrincipalId); + + appAgent + .put(`/submissions/${assessmentSubmissions[9].id}`) + .send(assessmentSubmissions[9]) + .expect(200, itemEnvelope(assessmentSubmissions[9]), err => { + expect(mockGetAssessmentSubmission).toHaveBeenCalledWith( + assessmentSubmissions[9].id, + true + ); + + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + assessmentSubmissions[9].assessment_id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].program_id + ); + + expect(mockGetAssessmentSubmission).toHaveBeenCalledWith( + assessmentSubmissions[9].id, + true, + false + ); + + done(err); + }); + }); + + it('should respond with a BadRequestError if given an invalid submission ID', done => { + const submissionId = 'test'; + + appAgent + .put(`/submissions/${submissionId}`) + .send(assessmentResponsesRows[7]) + .expect( + 400, + errorEnvelope( + `"${Number(submissionId)}" is not a valid submission ID.` + ), + done + ); + }); + + it('should respond with a ValidationError if given an invalid AssessmentSubmission', done => { + mockPrincipalId(participantPrincipalId); + + appAgent + .put(`/submissions/${assessmentSubmissions[9].id}`) + .send({ test: 'Hello' }) + .expect( + 422, + errorEnvelope(`Was not given a valid assessment submission.`), + done + ); + }); + + it('should respond with a NotFoundError if the submission ID was not found in the database', done => { + mockGetAssessmentSubmission.mockResolvedValue(null); + + mockPrincipalId(participantPrincipalId); + + appAgent + .put(`/submissions/${assessmentSubmissionId}`) + .send(assessmentSubmissions[13]) + .expect( + 404, + errorEnvelope( + `Could not find submission with ID ${assessmentSubmissionId}.` + ), + err => { + expect(mockGetAssessmentSubmission).toHaveBeenCalledWith( + assessmentSubmissionId, + true + ); + + done(err); + } + ); + }); + + it('should respond with an Unauthorized Error if user is not a member of the program of the submission', done => { + mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[9]); + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue(null); + + mockPrincipalId(participantPrincipalId); + + appAgent + .put(`/submissions/${assessmentSubmissions[9].id}`) + .send(assessmentSubmissions[13]) + .expect( + 401, + errorEnvelope( + `Could not access the assessment and submssion without enrollment in the program or being a facilitator.` + ), + err => { + expect(mockGetAssessmentSubmission).toHaveBeenCalledWith( + assessmentSubmissions[9].id, + true + ); + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + participantPrincipalId, + programAssessments[0].program_id + ); + done(err); + } + ); + }); + + it('should respond with an Unauthorized Error if user is participant but not the owner of the submission', done => { + mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[9]); + mockFindProgramAssessment.mockResolvedValue(programAssessments[0]); + mockGetPrincipalProgramRole.mockResolvedValue('Participant'); + + mockPrincipalId(otherParticipantPrincipalId); + + appAgent + .put(`/submissions/${assessmentSubmissions[9].id}`) + .send(assessmentSubmissions[13]) + .expect( + 401, + errorEnvelope( + `You may not update an assessment that is not your own.` + ), + err => { + expect(mockGetAssessmentSubmission).toHaveBeenCalledWith( + assessmentSubmissions[9].id, + true + ); + expect(mockFindProgramAssessment).toHaveBeenCalledWith( + programAssessments[0].id + ); + + expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith( + otherParticipantPrincipalId, + programAssessments[0].program_id + ); + done(err); + } + ); + }); + + it('should respond with a BadRequestError if submssion ID from param is not the same from request body', done => { + mockPrincipalId(participantPrincipalId); + + appAgent + .put(`/submissions/${assessmentSubmissionId}`) + .send(assessmentSubmissions[19]) + .expect( + 400, + errorEnvelope( + `The submission ID in the parameter (${assessmentSubmissionId}) is not the same ID as in the request body (${assessmentSubmissions[19].id}).` + ), + err => { + done(err); + } + ); + }); + + it('should respond with an internal server error if a database error occurs', done => { + mockGetAssessmentSubmission.mockRejectedValue(new Error()); + appAgent + .put(`/submissions/${assessmentSubmissionId}`) + .send(assessmentSubmissions[13]) + .expect(500, done); + }); + }); +}); diff --git a/api/src/middleware/assessmentsRouter.ts b/api/src/middleware/assessmentsRouter.ts new file mode 100644 index 00000000..7c955d11 --- /dev/null +++ b/api/src/middleware/assessmentsRouter.ts @@ -0,0 +1,979 @@ +import { Router } from 'express'; +import { DateTime } from 'luxon'; + +import { + BadRequestError, + ConflictError, + ForbiddenError, + InternalServerError, + NotFoundError, + UnauthorizedError, + ValidationError, +} from './httpErrors'; +import { collectionEnvelope, itemEnvelope } from './responseEnvelope'; + +import { + AssessmentDetails, + AssessmentSubmission, + AssessmentWithSubmissions, + AssessmentWithSummary, + CurriculumAssessment, + ProgramAssessment, + SavedAssessment, +} from '../models'; +import { + constructFacilitatorAssessmentSummary, + constructParticipantAssessmentSummary, + createAssessmentSubmission, + createCurriculumAssessment, + createProgramAssessment, + deleteCurriculumAssessment, + deleteProgramAssessment, + enrollFacilitator, + enrollParticipant, + facilitatorProgramIdsMatchingCurriculum, + findProgramAssessment, + getAssessmentSubmission, + getCurriculumAssessment, + getPrincipalProgramRole, + listAllProgramAssessmentSubmissions, + listParticipantProgramAssessmentSubmissions, + listPrincipalEnrolledProgramIds, + listProgramAssessments, + updateAssessmentSubmission, + updateCurriculumAssessment, + updateProgramAssessment, +} from '../services/assessmentsService'; +import { findConfig } from '../services/configService'; + +const assessmentsRouter = Router(); + +assessmentsRouter.get('/', async (req, res, next) => { + const { principalId } = req.session; + + try { + const programIds = await listPrincipalEnrolledProgramIds(principalId); + + const assessmentsSummaryList: AssessmentWithSummary[] = []; + + if (!programIds || programIds.length === 0) { + res.json(collectionEnvelope(assessmentsSummaryList, 0)); + return; + } + + for (const programId of programIds) { + const roleInProgram = await getPrincipalProgramRole( + principalId, + programId + ); + const programAssessments = await listProgramAssessments(programId); + + for (const programAssessment of programAssessments) { + if (roleInProgram === 'Participant') { + assessmentsSummaryList.push({ + curriculum_assessment: await getCurriculumAssessment( + programAssessment.assessment_id, + false, + false + ), + program_assessment: programAssessment, + participant_submissions_summary: + await constructParticipantAssessmentSummary( + principalId, + programAssessment + ), + principal_program_role: roleInProgram, + }); + } + + if (roleInProgram === 'Facilitator') { + assessmentsSummaryList.push({ + curriculum_assessment: await getCurriculumAssessment( + programAssessment.assessment_id, + false, + false + ), + program_assessment: programAssessment, + facilitator_submissions_summary: + await constructFacilitatorAssessmentSummary(programAssessment), + principal_program_role: roleInProgram, + }); + } + } + } + res.json( + collectionEnvelope(assessmentsSummaryList, assessmentsSummaryList.length) + ); + } catch (error) { + next(error); + return; + } +}); + +assessmentsRouter.get('/demo/facilitator', async (req, res, next) => { + const { principalId } = req.session; + + try { + if (await enrollFacilitator(principalId, 2)) { + res.redirect(findConfig('WEBAPP_ORIGIN', '') + '/assessments'); + } else { + res.status(204).send(); + } + } catch (error) { + next(error); + return; + } +}); + +assessmentsRouter.get('/demo/participant', async (req, res, next) => { + const { principalId } = req.session; + + try { + if (await enrollParticipant(principalId, 2)) { + res.redirect(findConfig('WEBAPP_ORIGIN', '') + '/assessments'); + } else { + res.status(204).send(); + } + } catch (error) { + next(error); + return; + } +}); + +assessmentsRouter.get( + '/curriculum/:curriculumAssessmentId', + async (req, res, next) => { + const { principalId } = req.session; + const { curriculumAssessmentId } = req.params; + const curriculumAssessmentIdParsed = Number(curriculumAssessmentId); + + if ( + !Number.isInteger(curriculumAssessmentIdParsed) || + curriculumAssessmentIdParsed < 1 + ) { + next( + new BadRequestError( + `"${curriculumAssessmentIdParsed}" is not a valid submission ID.` + ) + ); + return; + } + + try { + const includeQuestionsAndAllAnswers = true; + const includeQuestionsAndCorrectAnswers = true; + + const curriculumAssessment = await getCurriculumAssessment( + curriculumAssessmentIdParsed, + includeQuestionsAndAllAnswers, + includeQuestionsAndCorrectAnswers + ); + + if (!curriculumAssessment) { + throw new NotFoundError( + `Could not find curriculum assessment with ID ${curriculumAssessmentIdParsed}.` + ); + } + + const matchingProgramIds = await facilitatorProgramIdsMatchingCurriculum( + principalId, + curriculumAssessment.curriculum_id + ); + + if (matchingProgramIds.length === 0) { + throw new UnauthorizedError( + `Not allowed to access curriculum assessment with ID ${curriculumAssessmentIdParsed}.` + ); + } + + res.json(itemEnvelope(curriculumAssessment)); + } catch (err) { + next(err); + return; + } + } +); + +assessmentsRouter.post('/curriculum', async (req, res, next) => { + const { principalId } = req.session; + const curriculumAssessmentFromUser = req.body; + + const isACurriculumAssessment = ( + possibleAssessment: unknown + ): possibleAssessment is CurriculumAssessment => { + return (possibleAssessment as CurriculumAssessment).title !== undefined; + }; + if (!isACurriculumAssessment(curriculumAssessmentFromUser)) { + next(new ValidationError(`Was not given a valid curriculum assessment.`)); + return; + } + + try { + const facilitatorProgramIds = await facilitatorProgramIdsMatchingCurriculum( + principalId, + curriculumAssessmentFromUser.curriculum_id + ); + + if (facilitatorProgramIds.length === 0) { + throw new UnauthorizedError( + `Not allowed to add a new assessment for this curriculum.` + ); + } + + const curriculumAssessment = await createCurriculumAssessment( + curriculumAssessmentFromUser + ); + + res.status(201).json(itemEnvelope(curriculumAssessment)); + } catch (error) { + next(error); + return; + } +}); + +assessmentsRouter.put( + '/curriculum/:curriculumAssessmentId', + async (req, res, next) => { + const { principalId } = req.session; + const { curriculumAssessmentId } = req.params; + const curriculumAssessmentIdParsed = Number(curriculumAssessmentId); + + if ( + !Number.isInteger(curriculumAssessmentIdParsed) || + curriculumAssessmentIdParsed < 1 + ) { + next( + new BadRequestError( + `"${curriculumAssessmentIdParsed}" is not a valid curriculum assessment ID.` + ) + ); + return; + } + + const curriculumAssessmentFromUser = req.body; + + const isACurriculumAssessment = ( + possibleAssessment: unknown + ): possibleAssessment is CurriculumAssessment => { + return (possibleAssessment as CurriculumAssessment).id !== undefined; + }; + + if (!isACurriculumAssessment(curriculumAssessmentFromUser)) { + next(new ValidationError(`Was not given a valid curriculum assessment.`)); + return; + } + + try { + const curriculumAssessmentExisting = await getCurriculumAssessment( + curriculumAssessmentIdParsed + ); + + if (!curriculumAssessmentExisting) { + throw new NotFoundError( + `Could not find curriculum assessment with ID ${curriculumAssessmentIdParsed}.` + ); + } + + const matchingProgramAssessments = + await facilitatorProgramIdsMatchingCurriculum( + principalId, + curriculumAssessmentExisting.curriculum_id + ); + + if (matchingProgramAssessments.length === 0) { + throw new UnauthorizedError( + `Not allowed to make modifications to curriculum assessment with ID ${curriculumAssessmentIdParsed}.` + ); + } + + const updatedCurriculumAssessment: CurriculumAssessment = + await updateCurriculumAssessment(curriculumAssessmentFromUser); + + if (!updatedCurriculumAssessment) { + throw new InternalServerError( + `Could not update curriculum assessment with ID ${curriculumAssessmentIdParsed}.` + ); + } + + res.json(itemEnvelope(updatedCurriculumAssessment)); + } catch (err) { + next(err); + return; + } + } +); + +assessmentsRouter.delete( + '/curriculum/:curriculumAssessmentId', + async (req, res, next) => { + const { principalId } = req.session; + const { curriculumAssessmentId } = req.params; + const curriculumAssessmentIdParsed = Number(curriculumAssessmentId); + + if ( + !Number.isInteger(curriculumAssessmentIdParsed) || + curriculumAssessmentIdParsed < 1 + ) { + next( + new BadRequestError( + `"${curriculumAssessmentIdParsed}" is not a valid curriculum assessment ID.` + ) + ); + return; + } + + try { + const curriculumAssessmentExisting = await getCurriculumAssessment( + curriculumAssessmentIdParsed + ); + + if (!curriculumAssessmentExisting) { + throw new NotFoundError( + `Could not find curriculum assessment with ID ${curriculumAssessmentIdParsed}.` + ); + } + + const matchingPrograms = await facilitatorProgramIdsMatchingCurriculum( + principalId, + curriculumAssessmentExisting.curriculum_id + ); + + if (matchingPrograms.length === 0) { + throw new UnauthorizedError( + `Not allowed to delete curriculum assessment with ID ${curriculumAssessmentIdParsed}.` + ); + } + + await deleteCurriculumAssessment(curriculumAssessmentIdParsed).catch( + () => { + throw new ConflictError(`Cannot delete a curriculum assessment.`); + } + ); + + res.status(204).send(); + } catch (err) { + next(err); + return; + } + } +); + +assessmentsRouter.get( + '/program/:programAssessmentId', + async (req, res, next) => { + const { principalId } = req.session; + const { programAssessmentId } = req.params; + const programAssessmentIdParsed = Number(programAssessmentId); + + if ( + !Number.isInteger(programAssessmentIdParsed) || + programAssessmentIdParsed < 1 + ) { + next( + new BadRequestError( + `"${programAssessmentIdParsed}" is not a valid submission ID.` + ) + ); + return; + } + + try { + const programAssessment = await findProgramAssessment( + programAssessmentIdParsed + ); + + if (!programAssessment) { + throw new NotFoundError( + `Could not find program assessment with ID ${programAssessmentIdParsed}.` + ); + } + + const programRole = await getPrincipalProgramRole( + principalId, + programAssessment.program_id + ); + + if (programRole !== 'Facilitator') { + throw new UnauthorizedError( + `Could not access assessment with Program Assessment ID ${programAssessmentIdParsed}.` + ); + } + + const includeQuestionsAndAllAnswers = true; + const includeQuestionsAndCorrectAnswers = true; + + const curriculumAssessment = await getCurriculumAssessment( + programAssessment.assessment_id, + includeQuestionsAndAllAnswers, + includeQuestionsAndCorrectAnswers + ); + + const assessmentDetails: AssessmentDetails = { + curriculum_assessment: curriculumAssessment, + program_assessment: programAssessment, + }; + + res.json(itemEnvelope(assessmentDetails)); + } catch (err) { + next(err); + return; + } + } +); + +assessmentsRouter.post('/program', async (req, res, next) => { + const { principalId } = req.session; + const programAssessmentFromUser = req.body; + + let programAssessment; + try { + const isProgramAssessment = ( + possibleAssessment: unknown + ): possibleAssessment is ProgramAssessment => { + return (possibleAssessment as ProgramAssessment).program_id !== undefined; + }; + + if (!isProgramAssessment(programAssessmentFromUser)) { + throw new BadRequestError(`Was not given a valid program assessment.`); + } + + const programRole = await getPrincipalProgramRole( + principalId, + programAssessmentFromUser.program_id + ); + + if (programRole !== 'Facilitator') { + throw new UnauthorizedError( + `User is not allowed to create new program assessments for this program.` + ); + } + + programAssessment = await createProgramAssessment( + programAssessmentFromUser + ); + res.status(201).json(itemEnvelope(programAssessment)); + } catch (error) { + next(error); + return; + } +}); + +assessmentsRouter.put( + '/program/:programAssessmentId', + async (req, res, next) => { + const { programAssessmentId } = req.params; + const { principalId } = req.session; + const programAssessmentFromUser = req.body; + const programAssessmentIdParsed = Number(programAssessmentId); + + if ( + !Number.isInteger(programAssessmentIdParsed) || + programAssessmentIdParsed < 1 + ) { + next( + new BadRequestError( + `"${programAssessmentIdParsed}" is not a valid program assessment ID.` + ) + ); + return; + } + + let updatedPrgramAssessment; + try { + const programAssessment = await findProgramAssessment( + programAssessmentIdParsed + ); + + if (programAssessment === null) { + throw new NotFoundError( + `Could not find program assessment with ID ${programAssessmentIdParsed}.` + ); + } + + const programRole = await getPrincipalProgramRole( + principalId, + programAssessment.program_id + ); + + if (programRole !== 'Facilitator') { + next( + new UnauthorizedError( + `Could not access program Assessment with ID ${programAssessmentIdParsed}.` + ) + ); + return; + } + + const isprogramAssessment = ( + possibleAssessment: unknown + ): possibleAssessment is ProgramAssessment => { + return (possibleAssessment as ProgramAssessment).id !== undefined; + }; + + if (!isprogramAssessment(programAssessmentFromUser)) { + next(new BadRequestError(`Was not given a valid program assessment.`)); + return; + } + + updatedPrgramAssessment = await updateProgramAssessment( + programAssessmentFromUser + ); + res.json(itemEnvelope(updatedPrgramAssessment)); + } catch (error) { + next(error); + return; + } + } +); + +assessmentsRouter.delete( + '/program/:programAssessmentId', + async (req, res, next) => { + const { principalId } = req.session; + const { programAssessmentId } = req.params; + const programAssessmentIdParsed = Number(programAssessmentId); + + if ( + !Number.isInteger(programAssessmentIdParsed) || + programAssessmentIdParsed < 1 + ) { + next( + new BadRequestError( + `"${programAssessmentIdParsed}" is not a valid program assessment ID.` + ) + ); + return; + } + + try { + const matchingProgramAssessment = await findProgramAssessment( + programAssessmentIdParsed + ); + + if (matchingProgramAssessment === null) { + throw new NotFoundError( + `Could not find program assessment with ID ${programAssessmentIdParsed}.` + ); + } + + const programRole = await getPrincipalProgramRole( + principalId, + matchingProgramAssessment.program_id + ); + + if (programRole !== 'Facilitator') { + throw new UnauthorizedError( + `Not allowed to access program assessment with ID ${programAssessmentIdParsed}.` + ); + } + + await deleteProgramAssessment(programAssessmentIdParsed).catch(() => { + throw new ConflictError( + `Cannot delete a program assessment that has participant submissions.` + ); + }); + } catch (err) { + next(err); + return; + } + + res.status(204).send(); + } +); + +assessmentsRouter.get( + '/program/:programAssessmentId/submissions', + async (req, res, next) => { + const { principalId } = req.session; + const { programAssessmentId } = req.params; + const programAssessmentIdParsed = Number(programAssessmentId); + if ( + !Number.isInteger(programAssessmentIdParsed) || + programAssessmentIdParsed < 1 + ) { + next( + new BadRequestError( + `"${programAssessmentId}" is not a valid program assessment ID.` + ) + ); + return; + } + + let curriculumAssessment: CurriculumAssessment; + let programAssessment: ProgramAssessment; + let principalProgramRole: string; + let submissions: AssessmentSubmission[]; + + try { + programAssessment = await findProgramAssessment( + programAssessmentIdParsed + ); + + if (!programAssessment) { + throw new NotFoundError( + `Could not find program assessment with ID ${programAssessmentIdParsed}` + ); + } + + const programRole = await getPrincipalProgramRole( + principalId, + programAssessment.program_id + ); + + switch (programRole) { + case 'Participant': + submissions = await listParticipantProgramAssessmentSubmissions( + principalId, + programAssessment.id + ); + principalProgramRole = 'Participant'; + break; + case 'Facilitator': + submissions = await listAllProgramAssessmentSubmissions( + programAssessment.id + ); + principalProgramRole = 'Facilitator'; + break; + default: + throw new UnauthorizedError( + `Could not access program assessment with ID ${programAssessmentIdParsed} without enrollment.` + ); + } + + const includeQuestionsAndAllAnswers = false; + const includeQuestionsAndCorrectAnswers = false; + + curriculumAssessment = await getCurriculumAssessment( + programAssessment.assessment_id, + includeQuestionsAndAllAnswers, + includeQuestionsAndCorrectAnswers + ); + + const assessmentWithSubmissions: AssessmentWithSubmissions = { + curriculum_assessment: curriculumAssessment, + program_assessment: programAssessment, + principal_program_role: principalProgramRole, + submissions, + }; + + res.json(itemEnvelope(assessmentWithSubmissions)); + } catch (error) { + next(error); + return; + } + } +); + +assessmentsRouter.get( + '/program/:programAssessmentId/submissions/new', + async (req, res, next) => { + const { principalId } = req.session; + const { programAssessmentId } = req.params; + const programAssessmentIdParsed = Number(programAssessmentId); + + if ( + !Number.isInteger(programAssessmentIdParsed) || + programAssessmentIdParsed < 1 + ) { + next( + new BadRequestError( + `"${programAssessmentId}" is not a valid program assessment ID.` + ) + ); + return; + } + + try { + const programAssessment = await findProgramAssessment( + programAssessmentIdParsed + ); + + if (!programAssessment) { + throw new NotFoundError( + `Could not find program assessment with ID ${programAssessmentIdParsed}.` + ); + } + + if ( + DateTime.fromISO(programAssessment.available_after) > DateTime.now() + ) { + throw new ForbiddenError( + `Could not create a new submission of an assessment that's not yet available.` + ); + } + + if (DateTime.fromISO(programAssessment.due_date) < DateTime.now()) { + throw new ForbiddenError( + `Could not create a new submission of an assessment after its due date.` + ); + } + + const programRole = await getPrincipalProgramRole( + principalId, + programAssessment.program_id + ); + + if (!programRole) { + throw new UnauthorizedError( + `Could not access program assessment with ID ${programAssessmentIdParsed}) without enrollment.` + ); + } + + if (programRole === 'Facilitator') { + throw new UnauthorizedError( + `Facilitators are not allowed to create program assessment submissions.` + ); + } + + const includeQuestionsAndAllAnswers = true; + const includeQuestionsAndCorrectAnswers = false; + const curriculumAssessment = await getCurriculumAssessment( + programAssessment.assessment_id, + includeQuestionsAndAllAnswers, + includeQuestionsAndCorrectAnswers + ); + + const existingAssessmentSubmissions = + await listParticipantProgramAssessmentSubmissions( + principalId, + programAssessment.id + ); + + let assessmentSubmission: AssessmentSubmission; + let submissionCreated = false; + + if (!existingAssessmentSubmissions) { + assessmentSubmission = await createAssessmentSubmission( + principalId, + programAssessmentIdParsed, + programAssessment.assessment_id + ); + submissionCreated = true; + } else { + const inProgressSubmissions: AssessmentSubmission[] = + existingAssessmentSubmissions.filter(assessmentSubmission => + ['Opened', 'In Progress'].includes( + assessmentSubmission.assessment_submission_state + ) + ); + if (inProgressSubmissions.length !== 0) { + assessmentSubmission = await getAssessmentSubmission( + inProgressSubmissions[0].id, + true, + false + ); + } else if ( + existingAssessmentSubmissions.length >= + curriculumAssessment.max_num_submissions && + inProgressSubmissions.length === 0 + ) { + throw new ForbiddenError( + `Could not create a new submission as you have reached the maximum number of submissions for this assessment.` + ); + } else { + assessmentSubmission = await createAssessmentSubmission( + principalId, + programAssessmentIdParsed, + programAssessment.assessment_id + ); + submissionCreated = true; + } + } + + const assessmentWithSubmission: SavedAssessment = { + curriculum_assessment: curriculumAssessment, + program_assessment: programAssessment, + principal_program_role: programRole, + submission: assessmentSubmission, + }; + + if (submissionCreated === true) { + res.status(201).json(itemEnvelope(assessmentWithSubmission)); + } else { + res.json(itemEnvelope(assessmentWithSubmission)); + } + } catch (err) { + next(err); + return; + } + } +); + +assessmentsRouter.get('/submissions/:submissionId', async (req, res, next) => { + const { principalId } = req.session; + const { submissionId } = req.params; + const submissionIdParsed = Number(submissionId); + + if (!Number.isInteger(submissionIdParsed) || submissionIdParsed < 1) { + next( + new BadRequestError( + `"${submissionIdParsed}" is not a valid submission ID.` + ) + ); + return; + } + + try { + const assessmentSubmission = await getAssessmentSubmission( + submissionIdParsed, + true + ); + + if (!assessmentSubmission) { + next( + new NotFoundError( + `Could not find submission with ID ${submissionIdParsed}.` + ) + ); + return; + } + + const programAssessmentId = assessmentSubmission.assessment_id; + const programAssessment = await findProgramAssessment(programAssessmentId); + + const programRole = await getPrincipalProgramRole( + principalId, + programAssessment.program_id + ); + + if (!programRole) { + next( + new UnauthorizedError( + `Could not access submission with ID ${submissionIdParsed}.` + ) + ); + return; + } + + if (programRole === 'Participant') { + if (principalId !== assessmentSubmission.principal_id) { + next( + new UnauthorizedError( + `Could not access submission with ID ${submissionIdParsed}.` + ) + ); + return; + } + } + + const includeQuestionsAndAllAnswers = true; + const includeQuestionsAndCorrectAnswers = + programRole === 'Facilitator' || + assessmentSubmission.assessment_submission_state === 'Graded'; + + const curriculumAssessment = await getCurriculumAssessment( + programAssessment.assessment_id, + includeQuestionsAndAllAnswers, + includeQuestionsAndCorrectAnswers + ); + + const assessmentWithSubmission: SavedAssessment = { + curriculum_assessment: curriculumAssessment, + program_assessment: programAssessment, + principal_program_role: programRole, + submission: assessmentSubmission, + }; + + res.json(itemEnvelope(assessmentWithSubmission)); + } catch (err) { + next(err); + return; + } +}); + +assessmentsRouter.put('/submissions/:submissionId', async (req, res, next) => { + const { principalId } = req.session; + const { submissionId } = req.params; + const submissionIdParsed = Number(submissionId); + const submissionFromUser = req.body; + + try { + if (!Number.isInteger(submissionIdParsed) || submissionIdParsed < 1) { + throw new BadRequestError( + `"${submissionIdParsed}" is not a valid submission ID.` + ); + } + + const isSubmission = ( + possibleSubmission: unknown + ): possibleSubmission is AssessmentSubmission => { + return (possibleSubmission as AssessmentSubmission).id !== undefined; + }; + + if (!isSubmission(submissionFromUser)) { + throw new ValidationError(`Was not given a valid assessment submission.`); + } + + if (submissionFromUser.id !== submissionIdParsed) { + throw new BadRequestError( + `The submission ID in the parameter (${submissionIdParsed}) is not the same ID as in the request body (${submissionFromUser.id}).` + ); + } + + const existingAssessmentSubmission = await getAssessmentSubmission( + submissionIdParsed, + true + ); + + if (!existingAssessmentSubmission) { + throw new NotFoundError( + `Could not find submission with ID ${submissionIdParsed}.` + ); + } + + const programAssessment = await findProgramAssessment( + submissionFromUser.assessment_id + ); + + const programRole = await getPrincipalProgramRole( + principalId, + programAssessment.program_id + ); + + if (!programRole) { + throw new UnauthorizedError( + `Could not access the assessment and submssion without enrollment in the program or being a facilitator.` + ); + } + + if ( + submissionFromUser.principal_id !== principalId && + programRole !== 'Facilitator' + ) { + throw new UnauthorizedError( + `You may not update an assessment that is not your own.` + ); + } + + let updatedSubmission: AssessmentSubmission; + + if (programRole === 'Facilitator') { + updatedSubmission = await updateAssessmentSubmission( + submissionFromUser, + programRole === 'Facilitator' + ); + } else if ( + ['Opened', 'In Progress'].includes( + existingAssessmentSubmission.assessment_submission_state + ) + ) { + updatedSubmission = await updateAssessmentSubmission( + submissionFromUser, + programRole === 'Facilitator' + ); + } else { + updatedSubmission = await getAssessmentSubmission( + existingAssessmentSubmission.id, + true, + programRole === 'Facilitator' + ); + } + + res.json(itemEnvelope(updatedSubmission)); + } catch (err) { + next(err); + return; + } +}); + +export default assessmentsRouter; diff --git a/api/src/middleware/httpErrors.ts b/api/src/middleware/httpErrors.ts index 72e0e3a5..21f429ab 100644 --- a/api/src/middleware/httpErrors.ts +++ b/api/src/middleware/httpErrors.ts @@ -20,6 +20,15 @@ UnauthorizedError.prototype.message = 'The requester does not have access to the resource.'; UnauthorizedError.prototype.status = 401; +export class ForbiddenError extends HttpError {} +ForbiddenError.prototype.message = 'Access to this resource is not allowed.'; +ForbiddenError.prototype.status = 403; + +export class ConflictError extends HttpError {} +ConflictError.prototype.message = + 'This request is in conflict with the state of the server.'; +ConflictError.prototype.status = 409; + export class ValidationError extends HttpError {} ValidationError.prototype.message = 'The provided data does not meet requirements.'; diff --git a/api/src/middleware/routerTestUtils.ts b/api/src/middleware/routerTestUtils.ts index 4e1efed5..f38e5c05 100644 --- a/api/src/middleware/routerTestUtils.ts +++ b/api/src/middleware/routerTestUtils.ts @@ -4,6 +4,7 @@ import express, { NextFunction, Request, Response, Router } from 'express'; import expressSession from 'express-session'; import { Server, Socket } from 'socket.io'; import { SuperAgent, SuperAgentRequest } from 'superagent'; +import { handleErrors } from './errorHandlingMiddleware'; declare module 'supertest' { interface SuperTest extends SuperAgent { @@ -44,5 +45,6 @@ export const createAppAgentForRouter = (router: Router): SuperAgentTest => { next(); }); app.use(router); + app.use(handleErrors(() => null)); return agent(server); }; diff --git a/api/src/models.d.ts b/api/src/models.d.ts index 9884876a..aa7f88a8 100644 --- a/api/src/models.d.ts +++ b/api/src/models.d.ts @@ -72,3 +72,111 @@ export interface ParticipantActivityForProgram { program_id: number; participant_activities: ParticipantActivityCompletionStatus[]; } + +export interface ProgramParticipantCompletionSummary { + program: Program; + principal_id: number; + total_score: number; +} + +export interface Answer { + id?: number; + question_id?: number; + title: string; + description?: string; + sort_order: number; + correct_answer?: boolean; +} + +export interface Question { + id?: number; + assessment_id?: number; + title: string; + description?: string; + question_type: string; + answers?: Answer[]; + correct_answer_id?: number; + max_score: number; + sort_order: number; +} + +export interface AssessmentResponse { + id?: number; + assessment_id: number; + submission_id: number; + question_id: number; + answer_id?: number; + response_text?: string; + score?: number; + grader_response?: string; +} + +export interface AssessmentSubmission { + id?: number; + assessment_id: number; + principal_id: number; + assessment_submission_state: string; + score?: number; + opened_at: string; + submitted_at?: string; + last_modified: string; + responses?: AssessmentResponse[]; +} + +export interface ParticipantAssessmentSubmissionsSummary { + principal_id: number; + highest_state: string; + total_num_submissions: number; + most_recent_submitted_date?: string; + highest_score?: number; +} + +export interface FacilitatorAssessmentSubmissionsSummary { + num_participants_with_submissions: number; + num_program_participants: number; + num_ungraded_submissions: number; +} + +export interface CurriculumAssessment { + id?: number; + title: string; + assessment_type: string; + description?: string; + max_score: number; + max_num_submissions: number; + time_limit?: number; + curriculum_id: number; + activity_id: number; + principal_id: number; + questions?: Question[]; +} + +export interface ProgramAssessment { + id?: number; + program_id: number; + assessment_id?: number; + available_after: string; + due_date: string; +} + +export interface AssessmentDetails { + curriculum_assessment: CurriculumAssessment; + program_assessment: ProgramAssessment; +} + +interface AssessmentWithRole extends AssessmentDetails { + principal_program_role: string; +} + +export interface AssessmentWithSummary extends AssessmentWithRole { + participant_submissions_summary?: ParticipantAssessmentSubmissionsSummary; + facilitator_submissions_summary?: FacilitatorAssessmentSubmissionsSummary; +} + +export interface SavedAssessment extends AssessmentWithRole { + submission: AssessmentSubmission; +} + +export interface AssessmentWithSubmissions extends AssessmentWithRole { + submissions: AssessmentSubmission[]; +} diff --git a/api/src/server.ts b/api/src/server.ts index f705fdaa..af4057d2 100644 --- a/api/src/server.ts +++ b/api/src/server.ts @@ -1,15 +1,16 @@ // istanbul ignore file /* eslint-disable @typescript-eslint/ban-ts-comment */ -import connectRedis from 'connect-redis'; +import RedisStore from 'connect-redis'; import cors from 'cors'; import { createClient as createRedisClient } from 'redis'; import { createServer } from 'http'; import express from 'express'; -import expressSession, { Session, SessionData } from 'express-session'; +import session, { Session, SessionData } from 'express-session'; import helmet from 'helmet'; import Rollbar from 'rollbar'; import { Server } from 'socket.io'; +import assessmentsRouter from './middleware/assessmentsRouter'; import authRouter from './middleware/authRouter'; import competenciesRouter from './middleware/competenciesRouter'; import docsRouter from './middleware/docsRouter'; @@ -86,35 +87,59 @@ const start = async () => { }); }); - const RedisStore = connectRedis(expressSession); + app.set('trust proxy', 1); + + app.use(helmet()); + app.use(express.json()); + + // Session information is stored in Redis for retrieval by both the + // webapp front-end and the api back-end. + const redisHost = findConfig('REDIS_HOST', 'localhost'); const redisClient = createRedisClient({ - host: findConfig('REDIS_HOST', 'localhost'), - }); - redisClient.on('connect', () => console.log(`redis client connected`)); - redisClient.on('error', error => console.log(`redis client error: ${error}`)); - - const sessionMiddleware = expressSession({ - cookie: { sameSite: true, secure }, - name: 'session_id', - resave: true, - saveUninitialized: true, - secret: findConfig('SESSION_SECRET', ''), - store: new RedisStore({ client: redisClient }), + url: `redis://${redisHost}`, }); - io.use((socket, next) => { - // This code was taken from the documentation for using Socket.IO with express-session: - // https://socket.io/docs/v4/faq/#usage-with-express-session - // This was not designed with typescript in mind so it shows that the types are incompatible - // @ts-ignore - sessionMiddleware(socket.request, {}, next); - }); + // Try connecting to Redis. If we can't, notify the user and fall back + // to in-memory session store, if possible. + try { + await redisClient.connect(); + + if (!redisClient.isOpen) { + throw new Error('trouble connecting to redis.'); + } else { + console.info('redis client connected'); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sessionStore = new (RedisStore as any)({ client: redisClient }); + + const sessionMiddleware = session({ + cookie: { sameSite: true, secure }, + name: 'session_id', + resave: true, + saveUninitialized: true, + secret: findConfig('SESSION_SECRET', ''), + store: sessionStore, + }); - app.set('trust proxy', 1); + app.use(sessionMiddleware); + } catch (err) { + console.error(`redis client error: ${err}`); + console.error("Have you run 'docker compose up' yet?"); + + const sessionMiddleware = session({ + cookie: { sameSite: true, secure }, + name: 'session_id', + resave: true, + saveUninitialized: true, + secret: findConfig('SESSION_SECRET', ''), + }); + + app.use(sessionMiddleware); + } + + redisClient.on('error', console.error); - app.use(helmet()); - app.use(express.json()); - app.use(sessionMiddleware); app.use((req, res, next) => { req.io = io; next(); @@ -124,6 +149,7 @@ const start = async () => { origin: findConfig('WEBAPP_ORIGIN', ''), }); app.use(withCors, authRouter); + app.use('/assessments', withCors, loggedIn, assessmentsRouter); app.use('/competencies', withCors, loggedIn, competenciesRouter); app.use('/docs', withCors, docsRouter); app.use('/meetings', withCors, loggedIn, meetingsRouter); diff --git a/api/src/services/__tests__/assessmentsService.ts b/api/src/services/__tests__/assessmentsService.ts new file mode 100644 index 00000000..5ac1a685 --- /dev/null +++ b/api/src/services/__tests__/assessmentsService.ts @@ -0,0 +1,3110 @@ +import { DateTime, Settings } from 'luxon'; + +import { + Answer, + AssessmentResponse, + AssessmentSubmission, + CurriculumAssessment, + FacilitatorAssessmentSubmissionsSummary, + ParticipantAssessmentSubmissionsSummary, + ProgramAssessment, + Question, +} from '../../models'; +import { mockQuery } from '../mockDb'; + +import { + constructFacilitatorAssessmentSummary, + constructParticipantAssessmentSummary, + createAssessmentSubmission, + createCurriculumAssessment, + createProgramAssessment, + deleteCurriculumAssessment, + deleteProgramAssessment, + enrollFacilitator, + enrollParticipant, + facilitatorProgramIdsMatchingCurriculum, + findProgramAssessment, + getAssessmentSubmission, + getCurriculumAssessment, + getPrincipalProgramRole, + listAllProgramAssessmentSubmissions, + listParticipantProgramAssessmentSubmissions, + listPrincipalEnrolledProgramIds, + listProgramAssessments, + removeGradingInformation, + updateAssessmentSubmission, + updateCurriculumAssessment, + updateProgramAssessment, +} from '../assessmentsService'; + +/* EXAMPLE DATA: Variables */ + +const participantPrincipalId = 30; +const unenrolledPrincipalId = 31; +const facilitatorPrincipalId = 300; +const curriculumAssessmentId = 8; +const programAssessmentId = 16; +const singleChoiceQuestionId = 24; +const freeResponseQuestionId = 24; +const singleChoiceAnswerId = 28; +const freeResponseCorrectAnswerId = 29; +const assessmentSubmissionId = 32; +const assessmentSubmissionResponseSCId = 320; +const assessmentSubmissionResponseFRId = 321; + +/* EXAMPLE DATA: Database Rows */ + +const programParticipantRolesRows = [ + { id: 1, title: 'Facilitator' }, + { id: 2, title: 'Participant' }, +]; + +const curriculumAssessmentsRows = [ + { + id: 8, + title: 'Assignment 1: React', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + }, + { + id: 9, + title: 'New Curriculum Quiz', + description: null as string, + max_score: 42, + max_num_submissions: 13, + time_limit: 60, + curriculum_id: 4, + activity_id: 200, + principal_id: 300, + }, +]; + +const assessmentQuestionsRows = [ + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null as string, + question_type: 'single choice', + correct_answer_id: 28, + max_score: 1, + sort_order: 1, + }, + { + id: 24, + assessment_id: 8, + title: + 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?', + description: null as string, + question_type: 'free response', + correct_answer_id: 29, + max_score: 1, + sort_order: 1, + }, +]; + +const assessmentAnswersRows = [ + { + id: 28, + question_id: 24, + title: 'A relational database management system', + description: null as string, + sort_order: 1, + correct_answer: true, + }, + { + id: 29, + question_id: 24, + title: '

Hello, World!

', + description: null as string, + sort_order: 1, + correct_answer: true, + }, + { + id: 28, + question_id: 24, + title: 'A relational database management system', + description: 'Also known as a DBMS.', + sort_order: 1, + correct_answer: true, + }, +]; + +const programsRows = [ + { + id: 12, + title: 'Cohort 4', + start_date: '2022-10-24', + end_date: '2022-12-16', + time_zone: 'America/Vancouver', + curriculum_id: 4, + }, +]; + +const programAssessmentsRows = [ + { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06 00:00:00', + due_date: '2050-06-24 00:00:00', + }, + { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06 00:00:00', + due_date: '2023-02-10 00:00:00', + }, + { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2050-06-24 00:00:00', + due_date: '2050-06-23 00:00:00', + }, + { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06 00:00:00', + due_date: '2050-06-26 00:00:00', + }, + { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06 00:00:00', + due_date: '2050-06-24 00:00:00', + }, +]; + +const assessmentSubmissionsRows = [ + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Opened', + opened_at: '2023-02-09 12:00:00', + submitted_at: null as string, + updated_at: '2023-02-09 12:00:00', + score: null as number, + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'In Progress', + opened_at: '2023-02-09 12:00:00', + submitted_at: null as string, + updated_at: '2023-02-09 12:00:00', + score: null as number, + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Expired', + opened_at: '2023-02-09 12:00:00', + submitted_at: null as string, + updated_at: '2023-02-09 14:00:00', + score: null as number, + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Submitted', + opened_at: '2023-02-09 12:00:00', + submitted_at: '2023-02-09 13:23:45', + updated_at: '2023-02-09 13:23:45', + score: null as number, + }, + { + id: 36, + assessment_id: 16, + principal_id: 32, + assessment_submission_state: 'Submitted', + opened_at: '2023-02-09 12:01:00', + last_modified: '2023-02-09 12:01:00', + submitted_at: '2023-02-09 13:23:45', + updated_at: '2023-02-09 13:23:45', + score: null as number, + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Graded', + opened_at: '2023-02-09 12:00:00', + submitted_at: '2023-02-09 13:23:45', + updated_at: '2023-02-09 13:23:45', + score: 4, + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Submitted', + opened_at: '2023-02-09 12:00:00', + submitted_at: '2023-02-09 13:23:45', + updated_at: '2023-02-09 12:00:00', + score: null as number, + last_modified: '2023-02-09 13:23:45', + }, +]; + +const assessmentResponsesRows = [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: null as number, + response: null as string, + score: null as number, + grader_response: null as string, + }, + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + response: null as string, + score: null as number, + grader_response: null as string, + }, + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + response: null as string, + score: null as number, + grader_response: null as string, + }, + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + response: null as string, + score: 1, + grader_response: 'Well done!', + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: null as number, + response: null as string, + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: null as number, + response: '
Hello world!
', + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: null as number, + response: '
Hello world!
', + score: 0, + grader_response: 'Very close!', + }, + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + score: null as number, + grader_response: 'Well done!', + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + response: '
Hello world!
', + score: 0, + grader_response: 'Very close!', + }, +]; + +/* EXAMPLE DATA: Structured Data */ + +const curriculumAssessments: CurriculumAssessment[] = [ + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + id: 28, + question_id: 24, + description: null, + title: 'A relational database management system', + sort_order: 1, + }, + ], + }, + ], + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 3, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + id: 28, + question_id: 24, + description: null, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + correct_answer_id: 28, + }, + ], + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + id: 28, + question_id: 24, + description: null, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + correct_answer_id: 28, + }, + ], + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + id: 24, + assessment_id: 8, + title: + 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?', + description: null, + question_type: 'free response', + answers: [ + { + id: 29, + question_id: 24, + description: null, + title: '

Hello, World!

', + sort_order: 1, + correct_answer: true, + }, + ], + correct_answer_id: 29, + max_score: 1, + sort_order: 1, + }, + ], + }, + { + title: 'New Curriculum Quiz', + assessment_type: 'quiz', + description: null as string, + max_score: 42, + max_num_submissions: 13, + time_limit: 60, + curriculum_id: 4, + activity_id: 200, + principal_id: 300, + }, + { + title: 'New Curriculum Quiz', + assessment_type: 'quiz', + description: null, + max_score: 42, + max_num_submissions: 13, + time_limit: 60, + curriculum_id: 4, + activity_id: 200, + principal_id: 300, + questions: [ + { + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + answers: [ + { + description: null, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + max_score: 1, + sort_order: 1, + }, + ], + }, + { + title: 'New Curriculum Quiz', + assessment_type: 'quiz', + description: null, + max_score: 42, + max_num_submissions: 13, + time_limit: 60, + curriculum_id: 4, + activity_id: 200, + principal_id: 300, + questions: [ + { + assessment_id: 8, + title: + 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?', + description: null, + question_type: 'free response', + answers: [ + { + description: null, + title: '

Hello, World!

', + sort_order: 1, + correct_answer: true, + }, + ], + max_score: 1, + sort_order: 1, + }, + ], + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 121, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + answers: [ + { + description: null, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + max_score: 1, + sort_order: 1, + }, + ], + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + answers: [ + { + description: null, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + max_score: 1, + sort_order: 1, + }, + ], + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + description: null, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + }, + ], + }, + { + id: 8, + title: 'Assignment 1: React', + assessment_type: 'test', + description: 'Your assignment for week 1 learning.', + max_score: 10, + max_num_submissions: 1, + time_limit: 120, + curriculum_id: 4, + activity_id: 20, + principal_id: 3, + questions: [ + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + id: 28, + question_id: 24, + description: 'Also known as a DBMS.', + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + correct_answer_id: 28, + }, + ], + }, + { + title: 'New Curriculum Quiz', + assessment_type: 'quiz', + description: null as string, + max_score: 42, + max_num_submissions: 13, + time_limit: 60, + curriculum_id: 4, + activity_id: 200, + principal_id: 300, + id: 9, + }, + { + title: 'New Curriculum Quiz', + assessment_type: 'quiz', + description: null, + max_score: 42, + max_num_submissions: 13, + time_limit: 60, + curriculum_id: 4, + activity_id: 200, + principal_id: 300, + id: 9, + questions: [ + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + id: 28, + question_id: 24, + description: null, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + correct_answer_id: 28, + }, + ], + }, + { + title: 'New Curriculum Quiz', + assessment_type: 'quiz', + description: null, + max_score: 42, + max_num_submissions: 13, + time_limit: 60, + curriculum_id: 4, + activity_id: 200, + principal_id: 300, + id: 9, + questions: [ + { + id: 24, + assessment_id: 8, + title: + 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?', + description: null, + question_type: 'free response', + answers: [ + { + id: 29, + question_id: 24, + description: null, + title: '

Hello, World!

', + sort_order: 1, + correct_answer: true, + }, + ], + correct_answer_id: 29, + max_score: 1, + sort_order: 1, + }, + ], + }, +]; + +const answers: Answer[] = [ + { + id: 28, + question_id: 24, + description: null as string, + title: 'A relational database management system', + sort_order: 1, + }, + { + id: 28, + question_id: 24, + description: null as string, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + { + id: 29, + question_id: 24, + description: null as string, + title: '

Hello, World!

', + sort_order: 1, + }, + { + id: 29, + question_id: 24, + description: null as string, + title: '

Hello, World!

', + sort_order: 1, + correct_answer: true, + }, + { + description: null as string, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + { + description: null as string, + title: '

Hello, World!

', + sort_order: 1, + correct_answer: true, + }, + { + id: 28, + question_id: 24, + description: 'Also known as a DBMS.', + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, +]; + +const questions: Question[] = [ + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null as string, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + }, + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null as string, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + id: 28, + question_id: 24, + description: null as string, + title: 'A relational database management system', + sort_order: 1, + }, + ], + }, + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null as string, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + id: 28, + question_id: 24, + description: null as string, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + correct_answer_id: 28, + }, + { + id: 24, + assessment_id: 8, + title: + 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?', + description: null as string, + question_type: 'free response', + answers: [ + { + id: 29, + question_id: 24, + description: null as string, + title: '

Hello, World!

', + sort_order: 1, + correct_answer: true, + }, + ], + correct_answer_id: 29, + max_score: 1, + sort_order: 1, + }, + { + assessment_id: 8, + title: 'What is React?', + description: null as string, + question_type: 'single choice', + answers: [ + { + description: null as string, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + max_score: 1, + sort_order: 1, + }, + { + assessment_id: 8, + title: + 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?', + description: null as string, + question_type: 'free response', + answers: [ + { + description: null as string, + title: '

Hello, World!

', + sort_order: 1, + correct_answer: true, + }, + ], + max_score: 1, + sort_order: 1, + }, + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null as string, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + description: null as string, + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + }, + { + id: 24, + assessment_id: 8, + title: 'What is React?', + description: null as string, + question_type: 'single choice', + max_score: 1, + sort_order: 1, + answers: [ + { + id: 28, + question_id: 24, + description: 'Also known as a DBMS.', + title: 'A relational database management system', + sort_order: 1, + correct_answer: true, + }, + ], + correct_answer_id: 28, + }, +]; + +const programAssessments: ProgramAssessment[] = [ + { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06T00:00:00.000-08:00', + due_date: '2050-06-24T00:00:00.000-07:00', + }, + { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06T00:00:00.000-08:00', + due_date: '2023-02-10T00:00:00.000-08:00', + }, + { + id: 16, + program_id: 12, + assessment_id: 8, + available_after: '2050-06-24T00:00:00.000-07:00', + due_date: '2050-06-23T00:00:00.000-07:00', + }, + { + program_id: 12, + assessment_id: 8, + available_after: '2023-02-06 00:00:00', + due_date: '2050-06-24 00:00:00', + }, +]; + +const participantSummaries: ParticipantAssessmentSubmissionsSummary[] = [ + { + principal_id: 30, + highest_state: 'Inactive', + total_num_submissions: 0, + }, + { + principal_id: 30, + highest_state: 'Expired', + total_num_submissions: 1, + }, + { + principal_id: 30, + highest_state: 'Active', + total_num_submissions: 0, + }, + { + principal_id: 30, + highest_state: 'Graded', + most_recent_submitted_date: '2023-02-09T13:23:45.000Z', + total_num_submissions: 1, + highest_score: 4, + }, +]; + +const facilitatorSummaries: FacilitatorAssessmentSubmissionsSummary[] = [ + { + num_participants_with_submissions: 8, + num_program_participants: 12, + num_ungraded_submissions: 6, + }, +]; + +const assessmentResponses: AssessmentResponse[] = [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + }, + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + score: 1, + grader_response: 'Well done!', + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + score: 0, + grader_response: 'Very close!', + }, + { + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + { + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + }, + { + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + id: 320, + score: null as number, + grader_response: null as string, + }, + { + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + id: 321, + score: null as number, + grader_response: null as string, + }, + { + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + id: 320, + score: 1, + grader_response: 'Well done!', + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + score: 0, + grader_response: 'Very close!', + }, +]; + +const assessmentSubmissions: AssessmentSubmission[] = [ + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Opened', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:00:00.000Z', + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Opened', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:00:00.000Z', + responses: [ + { id: 320, assessment_id: 16, submission_id: 32, question_id: 24 }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'In Progress', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:05:00.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'In Progress', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:05:00.000Z', + responses: [ + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'In Progress', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:05:00.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'In Progress', + opened_at: '2023-02-10T07:00:00.000Z', + last_modified: '2023-02-10T07:05:00.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Expired', + opened_at: '2023-02-10T07:00:00.000Z', + last_modified: '2023-02-17T08:00:10.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Expired', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T14:00:00.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Expired', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-16T14:00:10.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Submitted', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T13:23:45.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Submitted', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T13:23:45.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + responses: [ + { + id: 321, + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + }, + ], + }, + { + id: 36, + assessment_id: 16, + principal_id: 32, + assessment_submission_state: 'Submitted', + opened_at: '2023-02-09T12:01:00.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + last_modified: '2023-02-09T13:23:45.000Z', + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Graded', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T13:23:45.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + score: 4, + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Graded', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T13:23:45.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + score: 4, + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + score: 1, + grader_response: 'Well done!', + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Graded', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T13:23:45.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + responses: [ + { + id: 320, + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Opened', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:05:00.000Z', + responses: [ + { + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Opened', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-09T12:05:00.000Z', + responses: [ + { + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Submitted', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-10T13:23:45.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + responses: [ + { + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + id: 320, + score: null as number, + grader_response: null as string, + }, + ], + }, + { + id: 32, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Submitted', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-10T13:23:45.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + responses: [ + { + assessment_id: 16, + submission_id: 32, + question_id: 24, + response_text: '
Hello world!
', + id: 321, + score: null as number, + grader_response: null as string, + }, + ], + }, + { + id: 33, + assessment_id: 16, + principal_id: 30, + assessment_submission_state: 'Submitted', + opened_at: '2023-02-09T12:00:00.000Z', + last_modified: '2023-02-10T13:23:45.000Z', + submitted_at: '2023-02-09T13:23:45.000Z', + responses: [ + { + assessment_id: 16, + submission_id: 32, + question_id: 24, + answer_id: 28, + id: 320, + score: null as number, + grader_response: null as string, + }, + ], + }, +]; + +describe('constructFacilitatorAssessmentSummary', () => { + it('should gather the relevant information for constructing a FacilitatorAssessmentSubmissionsSummary for a given program assessment', async () => { + mockQuery( + 'select count(distinct `principal_id`) as `count` from `assessment_submissions` where `assessment_id` = ?', + [programAssessments[0].id], + [ + { + count: facilitatorSummaries[0].num_participants_with_submissions, + }, + ] + ); + mockQuery( + 'select count(`id`) as `count` from `program_participants` where `program_id` = ? and `role_id` = ?', + [programAssessments[0].program_id, 2], + [ + { + count: facilitatorSummaries[0].num_program_participants, + }, + ] + ); + mockQuery( + 'select count(`id`) as `count` from `assessment_submissions` where `assessment_id` = ? and `score` is null', + [programAssessments[0].id], + [ + { + count: facilitatorSummaries[0].num_ungraded_submissions, + }, + ] + ); + + expect( + await constructFacilitatorAssessmentSummary(programAssessments[0]) + ).toEqual(facilitatorSummaries[0]); + }); +}); + +describe('constructParticipantAssessmentSummary', () => { + it('should gather the relevant information for constructing a ParticipantAssessmentSubmissionsSummary for a given program assessment', async () => { + mockQuery( + 'select `assessment_submission_states`.`title` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submission_states`.`id` = `assessment_submissions`.`assessment_submission_state_id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ? order by `assessment_submissions`.`assessment_submission_state_id` desc limit ?', + [participantPrincipalId, programAssessments[0].id, 1], + [ + { + title: assessmentSubmissionsRows[5].assessment_submission_state, + }, + ] + ); + mockQuery( + 'select `submitted_at` from `assessment_submissions` where `principal_id` = ? and `assessment_id` = ? order by `submitted_at` desc limit ?', + [participantPrincipalId, programAssessments[0].id, 1], + [ + { + submitted_at: assessmentSubmissionsRows[5].submitted_at, + }, + ] + ); + mockQuery( + 'select `assessment_submissions`.`id` as `id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ?', + [participantPrincipalId, programAssessments[0].id], + [assessmentSubmissionsRows[5]] + ); + mockQuery( + 'select `score` from `assessment_submissions` where `principal_id` = ? and `assessment_id` = ? order by `score` desc limit ?', + [participantPrincipalId, programAssessments[0].id, 1], + [{ score: assessmentSubmissionsRows[5].score }] + ); + + expect( + await constructParticipantAssessmentSummary( + participantPrincipalId, + programAssessments[0] + ) + ).toEqual(participantSummaries[3]); + }); + + it('should gather the relevant information for constructing a ParticipantAssessmentSubmissionsSummary even if no submissions, before assessment is active', async () => { + mockQuery( + 'select `assessment_submission_states`.`title` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submission_states`.`id` = `assessment_submissions`.`assessment_submission_state_id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ? order by `assessment_submissions`.`assessment_submission_state_id` desc limit ?', + [participantPrincipalId, programAssessments[0].id, 1], + [] + ); + mockQuery( + 'select `submitted_at` from `assessment_submissions` where `principal_id` = ? and `assessment_id` = ? order by `submitted_at` desc limit ?', + [participantPrincipalId, programAssessments[0].id, 1], + [] + ); + + mockQuery( + 'select `assessment_submissions`.`id` as `id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ?', + [participantPrincipalId, programAssessments[0].id], + [] + ); + mockQuery( + 'select `score` from `assessment_submissions` where `principal_id` = ? and `assessment_id` = ? order by `score` desc limit ?', + [participantPrincipalId, programAssessments[0].id, 1], + [] + ); + + expect( + await constructParticipantAssessmentSummary( + participantPrincipalId, + programAssessments[2] + ) + ).toEqual(participantSummaries[0]); + }); + + it('should gather the relevant information for constructing a ParticipantAssessmentSubmissionsSummary even if no submissions, for an active assessment', async () => { + mockQuery( + 'select `assessment_submission_states`.`title` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submission_states`.`id` = `assessment_submissions`.`assessment_submission_state_id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ? order by `assessment_submissions`.`assessment_submission_state_id` desc limit ?', + [participantPrincipalId, programAssessments[0].id, 1], + [] + ); + mockQuery( + 'select `submitted_at` from `assessment_submissions` where `principal_id` = ? and `assessment_id` = ? order by `submitted_at` desc limit ?', + [participantPrincipalId, programAssessments[0].id, 1], + [] + ); + mockQuery( + 'select `assessment_submissions`.`id` as `id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ?', + [participantPrincipalId, programAssessments[0].id], + [] + ); + mockQuery( + 'select `score` from `assessment_submissions` where `principal_id` = ? and `assessment_id` = ? order by `score` desc limit ?', + [participantPrincipalId, programAssessments[0].id, 1], + [] + ); + + expect( + await constructParticipantAssessmentSummary( + participantPrincipalId, + programAssessments[0] + ) + ).toEqual(participantSummaries[2]); + }); + + it('should gather the relevant information for constructing a ParticipantAssessmentSubmissionsSummary even if no submissions, after assessment is due', async () => { + mockQuery( + 'select `assessment_submission_states`.`title` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submission_states`.`id` = `assessment_submissions`.`assessment_submission_state_id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ? order by `assessment_submissions`.`assessment_submission_state_id` desc limit ?', + [participantPrincipalId, programAssessments[0].id, 1], + [] + ); + mockQuery( + 'select `submitted_at` from `assessment_submissions` where `principal_id` = ? and `assessment_id` = ? order by `submitted_at` desc limit ?', + [participantPrincipalId, programAssessments[0].id, 1], + [] + ); + mockQuery( + 'select `assessment_submissions`.`id` as `id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ?', + [participantPrincipalId, programAssessments[0].id], + [1] + ); + mockQuery( + 'select `score` from `assessment_submissions` where `principal_id` = ? and `assessment_id` = ? order by `score` desc limit ?', + [participantPrincipalId, programAssessments[0].id, 1], + [] + ); + + expect( + await constructParticipantAssessmentSummary( + participantPrincipalId, + programAssessments[1] + ) + ).toEqual(participantSummaries[1]); + }); +}); + +describe('createAssessmentSubmission', () => { + it('should create a new AssessmentSubmission for a program assessment', async () => { + const expectedNow = DateTime.utc(2023, 2, 9, 12, 0, 0); + Settings.now = () => expectedNow.toMillis(); + + mockQuery( + 'select `id` from `assessment_submission_states` where `title` = ?', + ['Opened'], + [{ id: 3 }] + ); + mockQuery( + 'insert into `assessment_submissions` (`assessment_id`, `assessment_submission_state_id`, `principal_id`) values (?, ?, ?)', + [programAssessments[0].id, 3, participantPrincipalId], + [assessmentSubmissions[1].id] + ); + mockQuery( + 'select `id` from `assessment_questions` where `assessment_id` = ?', + [programAssessments[0].assessment_id], + [{ id: singleChoiceQuestionId }] + ); + mockQuery( + 'insert into `assessment_responses` (`answer_id`, `assessment_id`, `question_id`, `response`, `submission_id`) values (DEFAULT, ?, ?, DEFAULT, ?)', + [ + programAssessments[0].id, + singleChoiceQuestionId, + assessmentSubmissions[1].id, + ], + [assessmentSubmissionResponseSCId] + ); + + expect( + await createAssessmentSubmission( + participantPrincipalId, + programAssessments[0].id, + programAssessments[0].assessment_id + ) + ).toEqual(assessmentSubmissions[1]); + }); +}); + +describe('createCurriculumAssessment', () => { + it('should create a curriculum assessment ID without question', async () => { + mockQuery( + 'insert into `curriculum_assessments` (`activity_id`, `curriculum_id`, `description`, `max_num_submissions`, `max_score`, `principal_id`, `time_limit`, `title`) values (?, ?, ?, ?, ?, ?, ?, ?)', + [ + curriculumAssessments[5].activity_id, + curriculumAssessments[5].curriculum_id, + curriculumAssessments[5].description, + curriculumAssessments[5].max_num_submissions, + curriculumAssessments[5].max_score, + curriculumAssessments[5].principal_id, + curriculumAssessments[5].time_limit, + curriculumAssessments[5].title, + ], + [curriculumAssessments[13].id] + ); + expect(await createCurriculumAssessment(curriculumAssessments[5])).toEqual( + curriculumAssessments[13] + ); + }); + + it('should create a curriculum assessment ID with a single choice question', async () => { + mockQuery( + 'insert into `curriculum_assessments` (`activity_id`, `curriculum_id`, `description`, `max_num_submissions`, `max_score`, `principal_id`, `time_limit`, `title`) values (?, ?, ?, ?, ?, ?, ?, ?)', + [ + curriculumAssessments[6].activity_id, + curriculumAssessments[6].curriculum_id, + curriculumAssessments[6].description, + curriculumAssessments[6].max_num_submissions, + curriculumAssessments[6].max_score, + curriculumAssessments[6].principal_id, + curriculumAssessments[6].time_limit, + curriculumAssessments[6].title, + ], + [curriculumAssessments[14].id] + ); + mockQuery( + 'insert into `assessment_questions` (`assessment_id`, `description`, `max_score`, `question_type_id`, `sort_order`, `title`) values (?, ?, ?, ?, ?, ?)', + [ + curriculumAssessments[14].id, + questions[4].description, + questions[4].max_score, + 1, + questions[4].sort_order, + questions[4].title, + ], + [singleChoiceQuestionId] + ); + mockQuery( + 'insert into `assessment_answers` (`description`, `question_id`, `sort_order`, `title`) values (?, ?, ?, ?)', + [ + answers[4].description, + singleChoiceQuestionId, + answers[4].sort_order, + answers[4].title, + ], + [singleChoiceAnswerId] + ); + mockQuery( + 'update `assessment_questions` set `correct_answer_id` = ? where `id` = ?', + [singleChoiceAnswerId, singleChoiceQuestionId], + [] + ); + expect(await createCurriculumAssessment(curriculumAssessments[6])).toEqual( + curriculumAssessments[14] + ); + }); + + it('should create a curriculum assessment ID with a free response question', async () => { + mockQuery( + 'insert into `curriculum_assessments` (`activity_id`, `curriculum_id`, `description`, `max_num_submissions`, `max_score`, `principal_id`, `time_limit`, `title`) values (?, ?, ?, ?, ?, ?, ?, ?)', + [ + curriculumAssessments[7].activity_id, + curriculumAssessments[7].curriculum_id, + curriculumAssessments[7].description, + curriculumAssessments[7].max_num_submissions, + curriculumAssessments[7].max_score, + curriculumAssessments[7].principal_id, + curriculumAssessments[7].time_limit, + curriculumAssessments[7].title, + ], + [curriculumAssessments[15].id] + ); + mockQuery( + 'insert into `assessment_questions` (`assessment_id`, `description`, `max_score`, `question_type_id`, `sort_order`, `title`) values (?, ?, ?, ?, ?, ?)', + [ + curriculumAssessments[15].id, + questions[5].description, + questions[5].max_score, + 2, + questions[5].sort_order, + questions[5].title, + ], + [freeResponseQuestionId] + ); + mockQuery( + 'insert into `assessment_answers` (`description`, `question_id`, `sort_order`, `title`) values (?, ?, ?, ?)', + [ + answers[5].description, + freeResponseQuestionId, + answers[5].sort_order, + answers[5].title, + ], + [freeResponseCorrectAnswerId] + ); + mockQuery( + 'update `assessment_questions` set `correct_answer_id` = ? where `id` = ?', + [freeResponseCorrectAnswerId, freeResponseQuestionId], + [] + ); + expect(await createCurriculumAssessment(curriculumAssessments[7])).toEqual( + curriculumAssessments[15] + ); + }); +}); + +describe('createProgramAssessment', () => { + it('should insert a ProgramAssessment into the database', async () => { + mockQuery( + 'insert into `program_assessments` (`assessment_id`, `available_after`, `due_date`, `program_id`) values (?, ?, ?, ?)', + [ + programAssessments[3].assessment_id, + programAssessments[3].available_after, + programAssessments[3].due_date, + programAssessments[3].program_id, + ], + [programAssessmentsRows[4].id] + ); + + mockQuery( + 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?', + [programAssessments[3].program_id], + [programsRows[0]] + ); + + expect(await createProgramAssessment(programAssessments[3])).toEqual( + programAssessments[0] + ); + }); +}); + +describe('deleteCurriculumAssessment', () => { + it('should delete a CurriculumAssessment from the database', async () => { + mockQuery( + 'delete from `curriculum_assessments` where `id` = ?', + [curriculumAssessmentId], + [1] + ); + + expect(await deleteCurriculumAssessment(curriculumAssessmentId)).toEqual([ + 1, + ]); + }); +}); + +describe('deleteProgramAssessment', () => { + it('should delete a ProgramAssessment from the database', async () => { + mockQuery( + 'delete from `program_assessments` where `id` = ?', + [programAssessmentId], + [1] + ); + + expect(await deleteProgramAssessment(programAssessmentId)).toEqual([1]); + }); +}); + +describe('enrollFacilitator', () => { + it('should enroll a principal ID into a program as a facilitator', async () => { + mockQuery( + 'select `role_id` from `program_participants` where `principal_id` = ? and `program_id` = ?', + [facilitatorPrincipalId, programAssessments[0].program_id], + [] + ); + mockQuery( + 'insert into `program_participants` (`principal_id`, `program_id`, `role_id`) values (?, ?, ?)', + [facilitatorPrincipalId, programAssessments[0].program_id, 1], + [1] + ); + + expect( + await enrollFacilitator( + facilitatorPrincipalId, + programAssessments[0].program_id + ) + ).toEqual(true); + }); + + it('should switch a participant role ID into a facilitator for a given program', async () => { + mockQuery( + 'select `role_id` from `program_participants` where `principal_id` = ? and `program_id` = ?', + [facilitatorPrincipalId, programAssessments[0].program_id], + [{ role_id: 2 }] + ); + mockQuery( + 'update `program_participants` set `role_id` = ? where `principal_id` = ? and `program_id` = ?', + [1, facilitatorPrincipalId, programAssessments[0].program_id], + [1] + ); + + expect( + await enrollFacilitator( + facilitatorPrincipalId, + programAssessments[0].program_id + ) + ).toEqual(true); + }); + + it('should do nothing if the user is already a facilitator of that program', async () => { + mockQuery( + 'select `role_id` from `program_participants` where `principal_id` = ? and `program_id` = ?', + [facilitatorPrincipalId, programAssessments[0].program_id], + [{ role_id: 1 }] + ); + + expect( + await enrollFacilitator( + facilitatorPrincipalId, + programAssessments[0].program_id + ) + ).toEqual(false); + }); +}); + +describe('enrollParticipant', () => { + it('should enroll a principal ID into a program as a participant', async () => { + mockQuery( + 'select `role_id` from `program_participants` where `principal_id` = ? and `program_id` = ?', + [participantPrincipalId, programAssessments[0].program_id], + [] + ); + mockQuery( + 'insert into `program_participants` (`principal_id`, `program_id`, `role_id`) values (?, ?, ?)', + [participantPrincipalId, programAssessments[0].program_id, 2], + [1] + ); + + expect( + await enrollParticipant( + participantPrincipalId, + programAssessments[0].program_id + ) + ).toEqual(true); + }); + + it('should switch a facilitator role ID into a participant for a given program', async () => { + mockQuery( + 'select `role_id` from `program_participants` where `principal_id` = ? and `program_id` = ?', + [participantPrincipalId, programAssessments[0].program_id], + [{ role_id: 1 }] + ); + mockQuery( + 'update `program_participants` set `role_id` = ? where `principal_id` = ? and `program_id` = ?', + [2, participantPrincipalId, programAssessments[0].program_id], + [1] + ); + + expect( + await enrollParticipant( + participantPrincipalId, + programAssessments[0].program_id + ) + ).toEqual(true); + }); + + it('should do nothing if the user is already a facilitator of that program', async () => { + mockQuery( + 'select `role_id` from `program_participants` where `principal_id` = ? and `program_id` = ?', + [participantPrincipalId, programAssessments[0].program_id], + [{ role_id: 2 }] + ); + + expect( + await enrollParticipant( + participantPrincipalId, + programAssessments[0].program_id + ) + ).toEqual(false); + }); +}); + +describe('facilitatorProgramIdsMatchingCurriculum', () => { + it('should return an array of program IDs for a principal that is facilitator of at least one program', async () => { + mockQuery( + 'select `program_id` from `program_participants` where `principal_id` = ?', + [facilitatorPrincipalId], + [{ program_id: programAssessments[0].program_id }] + ); + mockQuery( + 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `curriculum_id` = ?', + [curriculumAssessments[0].curriculum_id], + [programsRows[0]] + ); + mockQuery( + 'select `program_participant_roles`.`title` from `program_participant_roles` inner join `program_participants` on `program_participant_roles`.`id` = `program_participants`.`role_id` where `principal_id` = ? and `program_id` = ?', + [facilitatorPrincipalId, programAssessments[0].program_id], + [{ title: 'Facilitator' }] + ); + + expect( + await facilitatorProgramIdsMatchingCurriculum( + facilitatorPrincipalId, + curriculumAssessments[0].curriculum_id + ) + ).toEqual([programAssessments[0].program_id]); + }); + + it('should return an empty array of program IDs for a principal that is not a facilitator of at least one program', async () => { + mockQuery( + 'select `program_id` from `program_participants` where `principal_id` = ?', + [participantPrincipalId], + [] + ); + expect( + await facilitatorProgramIdsMatchingCurriculum( + participantPrincipalId, + curriculumAssessments[0].curriculum_id + ) + ).toEqual([]); + }); +}); + +describe('findProgramAssessment', () => { + it('should return a ProgramAssessment for an existing program assessment ID', async () => { + mockQuery( + 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?', + [programAssessments[0].id], + [programAssessmentsRows[0]] + ); + + mockQuery( + 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?', + [programAssessments[0].program_id], + [programsRows[0]] + ); + + expect(await findProgramAssessment(programAssessments[0].id)).toEqual( + programAssessments[0] + ); + }); + + it('should return null for a program assessment ID that does not exist', async () => { + mockQuery( + 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?', + [programAssessments[0].id], + [] + ); + + expect(await findProgramAssessment(programAssessments[0].id)).toEqual(null); + }); +}); + +describe('getAssessmentSubmission', () => { + it('should get assessment submission based on given submission ID', async () => { + const assessmentSubmissionId = assessmentSubmissions[13].id; + const responsesIncluded = true; + const gradingsIncluded = true; + + mockQuery( + 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?', + [assessmentSubmissionId], + [assessmentSubmissionsRows[5]] + ); + + mockQuery( + 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?', + [assessmentSubmissionId], + [assessmentResponsesRows[3]] + ); + + expect( + await getAssessmentSubmission( + assessmentSubmissionId, + responsesIncluded, + gradingsIncluded + ) + ).toEqual(assessmentSubmissions[13]); + }); + + it('should get assessment submission with null for responses (if no responses found) based on given submission ID', async () => { + const assessmentSubmissionId = assessmentSubmissionsRows[0].id; + const responsesIncluded = true; + const gradingsIncluded = true; + + mockQuery( + 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?', + [assessmentSubmissionId], + [assessmentSubmissionsRows[0]] + ); + + mockQuery( + 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?', + [assessmentSubmissionId], + [] + ); + + expect( + await getAssessmentSubmission( + assessmentSubmissionId, + responsesIncluded, + gradingsIncluded + ) + ).toEqual(assessmentSubmissions[0]); + }); + + it('should return null for a assessment submission ID that does not exist', async () => { + mockQuery( + 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?', + [assessmentSubmissions[13].id], + [] + ); + + expect( + await getAssessmentSubmission(assessmentSubmissions[13].id, true, true) + ).toEqual(null); + }); +}); + +describe('getCurriculumAssessment', () => { + it('should return a CurriculumAssessment for an existing curriculum assessment ID', async () => { + const questionsAndAllAnswersIncluded = true, + questionsAndCorrectAnswersIncluded = true; + + mockQuery( + 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?', + [curriculumAssessmentId], + [curriculumAssessmentsRows[0]] + ); + mockQuery( + 'select `activity_types`.`title` from `activity_types` inner join `activities` on `activities`.`activity_type_id` = `activity_types`.`id` where `activities`.`id` = ?', + [curriculumAssessmentsRows[0].activity_id], + [ + { + title: curriculumAssessments[3].assessment_type, + }, + ] + ); + mockQuery( + 'select `assessment_questions`.`id`, `assessment_questions`.`title`, `description`, `assessment_question_types`.`title` as `question_type`, `correct_answer_id`, `max_score`, `sort_order` from `assessment_questions` inner join `assessment_question_types` on `assessment_questions`.`question_type_id` = `assessment_question_types`.`id` where `assessment_questions`.`assessment_id` = ? order by `sort_order` asc', + [curriculumAssessments[3].id], + [assessmentQuestionsRows[0]] + ); + + const questionIds = [assessmentQuestionsRows[0].id]; + + mockQuery( + 'select `id`, `question_id`, `title`, `description`, `sort_order` from `assessment_answers` where `question_id` = ? order by `sort_order` asc', + [questionIds[0]], + [assessmentAnswersRows[0]] + ); + + expect( + await getCurriculumAssessment( + curriculumAssessments[3].id, + questionsAndAllAnswersIncluded, + questionsAndCorrectAnswersIncluded + ) + ).toEqual(curriculumAssessments[3]); + }); + + it('should return null for a curriculum assessment ID that does not exist', async () => { + mockQuery( + 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?', + [curriculumAssessmentId], + [] + ); + + expect( + await getCurriculumAssessment(curriculumAssessmentId, true, true) + ).toEqual(null); + }); +}); + +describe('getPrincipalProgramRole', () => { + it('should return the correct role for a facilitator based on principal ID and program ID', async () => { + mockQuery( + 'select `program_participant_roles`.`title` from `program_participant_roles` inner join `program_participants` on `program_participant_roles`.`id` = `program_participants`.`role_id` where `principal_id` = ? and `program_id` = ?', + [facilitatorPrincipalId, programAssessments[0].program_id], + [programParticipantRolesRows[0]] + ); + + expect( + await getPrincipalProgramRole( + facilitatorPrincipalId, + programAssessments[0].program_id + ) + ).toEqual('Facilitator'); + }); + + it('should return the correct role for a participant based on principal ID and program ID', async () => { + mockQuery( + 'select `program_participant_roles`.`title` from `program_participant_roles` inner join `program_participants` on `program_participant_roles`.`id` = `program_participants`.`role_id` where `principal_id` = ? and `program_id` = ?', + [participantPrincipalId, programAssessments[0].program_id], + [programParticipantRolesRows[1]] + ); + + expect( + await getPrincipalProgramRole( + participantPrincipalId, + programAssessments[0].program_id + ) + ).toEqual('Participant'); + }); + + it('should return null for a user not enrolled in the program', async () => { + mockQuery( + 'select `program_participant_roles`.`title` from `program_participant_roles` inner join `program_participants` on `program_participant_roles`.`id` = `program_participants`.`role_id` where `principal_id` = ? and `program_id` = ?', + [unenrolledPrincipalId, programAssessments[0].program_id], + [] + ); + + expect( + await getPrincipalProgramRole( + unenrolledPrincipalId, + programAssessments[0].program_id + ) + ).toEqual(null); + }); +}); + +describe('listAllProgramAssessmentSubmissions', () => { + it('should return all program assessment submissions for a given program assessment', async () => { + mockQuery( + 'select `assessment_submissions`.`id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`principal_id`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_id` = ?', + [assessmentSubmissions[0].assessment_id], + [ + assessmentSubmissionsRows[0], + assessmentSubmissionsRows[4], + assessmentSubmissionsRows[5], + ] + ); + + expect( + await listAllProgramAssessmentSubmissions( + assessmentSubmissions[0].assessment_id + ) + ).toEqual([ + assessmentSubmissions[0], + assessmentSubmissions[11], + assessmentSubmissions[12], + ]); + }); + + it('should return null if no program assessment submissions for a given program assessment', async () => { + mockQuery( + 'select `assessment_submissions`.`id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`principal_id`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_id` = ?', + [assessmentSubmissions[0].assessment_id], + [] + ); + + expect( + await listAllProgramAssessmentSubmissions( + assessmentSubmissions[0].assessment_id + ) + ).toEqual(null); + }); +}); + +describe('listParticipantProgramAssessmentSubmissions', () => { + it('should return program assessment submissions for a participant for a given program assessment', async () => { + mockQuery( + 'select `assessment_submissions`.`id` as `id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ?', + [participantPrincipalId, assessmentSubmissions[0].assessment_id], + [assessmentSubmissionsRows[0]] + ); + expect( + await listParticipantProgramAssessmentSubmissions( + participantPrincipalId, + assessmentSubmissions[0].assessment_id + ) + ).toEqual([assessmentSubmissions[0]]); + }); + + it('should return null if no program assessment submissions for a given program assessment', async () => { + mockQuery( + 'select `assessment_submissions`.`id` as `id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ?', + [participantPrincipalId, assessmentSubmissions[0].assessment_id], + [] + ); + + expect( + await listParticipantProgramAssessmentSubmissions( + participantPrincipalId, + assessmentSubmissions[0].assessment_id + ) + ).toEqual(null); + }); +}); + +describe('listPrincipalEnrolledProgramIds', () => { + const enrolledProgramsList = [{ program_id: 2 }]; + + it('should return program ID list for which a principal is facilitator', async () => { + mockQuery( + 'select `program_id` from `program_participants` where `principal_id` = ?', + [facilitatorPrincipalId], + enrolledProgramsList + ); + expect( + await listPrincipalEnrolledProgramIds(facilitatorPrincipalId) + ).toEqual([enrolledProgramsList[0].program_id]); + }); + + it('should return program ID list for which a principal is participant', async () => { + mockQuery( + 'select `program_id` from `program_participants` where `principal_id` = ?', + [participantPrincipalId], + enrolledProgramsList + ); + expect( + await listPrincipalEnrolledProgramIds(participantPrincipalId) + ).toEqual([enrolledProgramsList[0].program_id]); + }); +}); + +describe('listProgramAssessments', () => { + it('should return all ProgramAssessments linked to a program ID', async () => { + mockQuery( + 'select `id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `program_id` = ?', + [programAssessmentsRows[0].program_id], + [programAssessmentsRows[0]] + ); + + mockQuery( + 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?', + [programAssessmentsRows[0].program_id], + [programsRows[0]] + ); + + expect( + await listProgramAssessments(programAssessmentsRows[0].program_id) + ).toEqual([programAssessments[0]]); + }); + + it('should return null if no ProgramAssessments linked to a program ID were found', async () => { + mockQuery( + 'select `id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `program_id` = ?', + [programAssessmentsRows[0].program_id], + [] + ); + + expect( + await listProgramAssessments(programAssessmentsRows[0].program_id) + ).toEqual(null); + }); +}); + +describe('removeGradingInformation', () => { + it('should remove all grading-related information from an AssessmentSubmission', () => { + expect(removeGradingInformation(assessmentSubmissions[13])).toEqual( + assessmentSubmissions[14] + ); + }); +}); + +describe('updateAssessmentSubmission', () => { + it('should update an existing in-progress assessment submission by a participant with a changed response', async () => { + const expectedNow = DateTime.utc(2023, 2, 9, 12, 5, 0); + Settings.now = () => expectedNow.toMillis(); + + mockQuery( + 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?', + [assessmentSubmissionId], + [assessmentSubmissionsRows[1]] + ); + mockQuery( + 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?', + [assessmentSubmissionId], + [assessmentResponsesRows[0], assessmentResponsesRows[4]] + ); + mockQuery( + 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?', + [programAssessments[0].id], + [programAssessmentsRows[0]] + ); + mockQuery( + 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?', + [programAssessments[0].program_id], + [programsRows[0]] + ); + mockQuery( + 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?', + [curriculumAssessmentId], + [curriculumAssessmentsRows[0]] + ); + mockQuery( + 'select `activity_types`.`title` from `activity_types` inner join `activities` on `activities`.`activity_type_id` = `activity_types`.`id` where `activities`.`id` = ?', + [curriculumAssessmentsRows[0].activity_id], + [ + { + title: curriculumAssessments[3].assessment_type, + }, + ] + ); + mockQuery( + 'update `assessment_responses` set `answer_id` = ? where `id` = ?', + [assessmentResponsesRows[1].answer_id, assessmentResponsesRows[1].id], + 1 + ); + mockQuery( + 'update `assessment_responses` set `response` = ? where `id` = ?', + [assessmentResponsesRows[5].response, assessmentResponsesRows[5].id], + 1 + ); + mockQuery( + 'select `id` from `assessment_submission_states` where `title` = ?', + ['In Progress'], + [{ id: 4 }] + ); + mockQuery( + 'update `assessment_submissions` set `assessment_submission_state_id` = ? where `id` = ?', + [4, assessmentSubmissions[17].id], + 1 + ); + + expect( + await updateAssessmentSubmission(assessmentSubmissions[4], false) + ).toEqual(assessmentSubmissions[4]); + }); + + it('should update an existing submitted assessment submission by adding grading information from a facilitator', async () => { + const expectedNow = DateTime.utc(2023, 2, 9, 12, 5, 0); + Settings.now = () => expectedNow.toMillis(); + + mockQuery( + 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?', + [assessmentSubmissionId], + [assessmentSubmissionsRows[3]] + ); + + mockQuery( + 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?', + [assessmentSubmissionId], + [assessmentResponsesRows[1]] + ); + + mockQuery( + 'update `assessment_responses` set `score` = ?, `grader_response` = ? where `id` = ?', + [ + assessmentResponsesRows[3].score, + assessmentResponsesRows[3].grader_response, + assessmentResponsesRows[3].id, + ], + + 1 + ); + + mockQuery( + 'select `id` from `assessment_submission_states` where `title` = ?', + ['Graded'], + [{ id: 7 }] + ); + + mockQuery( + 'update `assessment_submissions` set `assessment_submission_state_id` = ?, `score` = ? where `id` = ?', + [7, assessmentSubmissions[13].score, assessmentSubmissions[13].id], + 1 + ); + + expect( + await updateAssessmentSubmission(assessmentSubmissions[13], true) + ).toEqual(assessmentSubmissions[13]); + }); + + it('should update an existing in-progress assessment submission by a participant with a new SC response', async () => { + const expectedNow = DateTime.utc(2023, 2, 9, 12, 5, 0); + Settings.now = () => expectedNow.toMillis(); + + mockQuery( + 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?', + [assessmentSubmissionId], + [assessmentSubmissionsRows[1]] + ); + mockQuery( + 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?', + [assessmentSubmissionId], + [] + ); + mockQuery( + 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?', + [programAssessments[0].id], + [programAssessmentsRows[0]] + ); + mockQuery( + 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?', + [programAssessments[0].program_id], + [programsRows[0]] + ); + mockQuery( + 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?', + [curriculumAssessmentId], + [curriculumAssessmentsRows[0]] + ); + mockQuery( + 'select `activity_types`.`title` from `activity_types` inner join `activities` on `activities`.`activity_type_id` = `activity_types`.`id` where `activities`.`id` = ?', + [curriculumAssessmentsRows[0].activity_id], + [ + { + title: curriculumAssessments[3].assessment_type, + }, + ] + ); + mockQuery( + 'insert into `assessment_responses` (`answer_id`, `assessment_id`, `question_id`, `response`, `submission_id`) values (?, ?, ?, DEFAULT, ?)', + [ + singleChoiceAnswerId, + programAssessmentId, + singleChoiceQuestionId, + assessmentSubmissions[2].id, + ], + [assessmentSubmissionResponseSCId] + ); + mockQuery( + 'select `id` from `assessment_submission_states` where `title` = ?', + ['In Progress'], + [{ id: 4 }] + ); + mockQuery( + 'update `assessment_submissions` set `assessment_submission_state_id` = ? where `id` = ?', + [4, assessmentSubmissions[2].id], + 1 + ); + + expect( + await updateAssessmentSubmission(assessmentSubmissions[15], false) + ).toEqual(assessmentSubmissions[2]); + }); + + it('should update an existing in-progress assessment submission by a participant with a new FR response', async () => { + const expectedNow = DateTime.utc(2023, 2, 9, 12, 5, 0); + Settings.now = () => expectedNow.toMillis(); + + mockQuery( + 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?', + [assessmentSubmissionId], + [assessmentSubmissionsRows[1]] + ); + mockQuery( + 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?', + [assessmentSubmissionId], + [] + ); + mockQuery( + 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?', + [programAssessments[0].id], + [programAssessmentsRows[0]] + ); + mockQuery( + 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?', + [programAssessments[0].program_id], + [programsRows[0]] + ); + mockQuery( + 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?', + [curriculumAssessmentId], + [curriculumAssessmentsRows[0]] + ); + mockQuery( + 'select `activity_types`.`title` from `activity_types` inner join `activities` on `activities`.`activity_type_id` = `activity_types`.`id` where `activities`.`id` = ?', + [curriculumAssessmentsRows[0].activity_id], + [ + { + title: curriculumAssessments[3].assessment_type, + }, + ] + ); + mockQuery( + 'insert into `assessment_responses` (`answer_id`, `assessment_id`, `question_id`, `response`, `submission_id`) values (DEFAULT, ?, ?, ?, ?)', + [ + programAssessmentId, + freeResponseQuestionId, + assessmentResponses[7].response_text, + assessmentSubmissions[2].id, + ], + [assessmentSubmissionResponseFRId] + ); + mockQuery( + 'select `id` from `assessment_submission_states` where `title` = ?', + ['In Progress'], + [{ id: 4 }] + ); + mockQuery( + 'update `assessment_submissions` set `assessment_submission_state_id` = ? where `id` = ?', + [4, assessmentSubmissions[2].id], + 1 + ); + + expect( + await updateAssessmentSubmission(assessmentSubmissions[16], false) + ).toEqual(assessmentSubmissions[3]); + }); + + it('should submit an assessment submission marked as being submitted by updating its state and submission date', async () => { + const expectedNow = DateTime.utc(2023, 2, 9, 13, 23, 45); + Settings.now = () => expectedNow.toMillis(); + + mockQuery( + 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?', + [assessmentSubmissionId], + [assessmentSubmissionsRows[1]] + ); + + mockQuery( + 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?', + [assessmentSubmissionId], + [assessmentResponsesRows[1]] + ); + + mockQuery( + 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?', + [programAssessments[0].id], + [programAssessmentsRows[0]] + ); + + mockQuery( + 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?', + [programAssessments[0].program_id], + [programsRows[0]] + ); + + mockQuery( + 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?', + [curriculumAssessmentId], + [curriculumAssessmentsRows[0]] + ); + + mockQuery( + 'select `activity_types`.`title` from `activity_types` inner join `activities` on `activities`.`activity_type_id` = `activity_types`.`id` where `activities`.`id` = ?', + [curriculumAssessmentsRows[0].activity_id], + [ + { + title: curriculumAssessments[3].assessment_type, + }, + ] + ); + + mockQuery( + 'select `id` from `assessment_submission_states` where `title` = ?', + ['Submitted'], + [{ id: 6 }] + ); + + mockQuery( + 'update `assessment_submissions` set `assessment_submission_state_id` = ?, `submitted_at` = CURRENT_TIMESTAMP where `id` = ?', + [6, assessmentSubmissions[9].id], + 1 + ); + + expect( + await updateAssessmentSubmission(assessmentSubmissions[9], false) + ).toEqual(assessmentSubmissions[9]); + }); + + it('should automatically expire an in-progress assessment submission after the due date', async () => { + const expectedNow = DateTime.utc(2023, 2, 10, 8, 0, 10); + Settings.now = () => expectedNow.toMillis(); + + mockQuery( + 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?', + [assessmentSubmissionId], + [assessmentSubmissionsRows[1]] + ); + mockQuery( + 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?', + [assessmentSubmissionId], + [assessmentResponsesRows[0], assessmentResponsesRows[5]] + ); + mockQuery( + 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?', + [programAssessments[1].id], + [programAssessmentsRows[1]] + ); + mockQuery( + 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?', + [programAssessments[1].program_id], + [programsRows[0]] + ); + mockQuery( + 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?', + [curriculumAssessmentId], + [curriculumAssessmentsRows[0]] + ); + mockQuery( + 'select `activity_types`.`title` from `activity_types` inner join `activities` on `activities`.`activity_type_id` = `activity_types`.`id` where `activities`.`id` = ?', + [curriculumAssessmentsRows[0].activity_id], + [ + { + title: curriculumAssessments[3].assessment_type, + }, + ] + ); + mockQuery( + 'select `id` from `assessment_submission_states` where `title` = ?', + ['Expired'], + [{ id: 5 }] + ); + mockQuery( + 'update `assessment_submissions` set `assessment_submission_state_id` = ? where `id` = ?', + [5, assessmentSubmissions[2].id], + [] + ); + + expect( + await updateAssessmentSubmission(assessmentSubmissions[5], false) + ).toEqual(assessmentSubmissions[6]); + }); + + it('should automatically expire an in-progress assessment submission after the time limit', async () => { + const expectedNow = DateTime.utc(2023, 2, 9, 14, 0, 10); + Settings.now = () => expectedNow.toMillis(); + + mockQuery( + 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?', + [assessmentSubmissionId], + [assessmentSubmissionsRows[1]] + ); + mockQuery( + 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?', + [assessmentSubmissionId], + [assessmentResponsesRows[0]] + ); + mockQuery( + 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?', + [programAssessments[0].id], + [programAssessmentsRows[0]] + ); + mockQuery( + 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?', + [programAssessments[1].program_id], + [programsRows[0]] + ); + mockQuery( + 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?', + [curriculumAssessmentId], + [curriculumAssessmentsRows[0]] + ); + mockQuery( + 'select `activity_types`.`title` from `activity_types` inner join `activities` on `activities`.`activity_type_id` = `activity_types`.`id` where `activities`.`id` = ?', + [curriculumAssessmentsRows[0].activity_id], + [ + { + title: curriculumAssessments[3].assessment_type, + }, + ] + ); + mockQuery( + 'select `id` from `assessment_submission_states` where `title` = ?', + ['Expired'], + [{ id: 5 }] + ); + mockQuery( + 'update `assessment_submissions` set `assessment_submission_state_id` = ? where `id` = ?', + [5, assessmentSubmissions[2].id], + [] + ); + + expect( + await updateAssessmentSubmission(assessmentSubmissions[2], false) + ).toEqual(assessmentSubmissions[8]); + }); + + it('should not allow a participant to modify their responses to an expired submission', async () => { + const expectedNow = DateTime.utc(2023, 2, 9, 12, 5, 0); + Settings.now = () => expectedNow.toMillis(); + + mockQuery( + 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?', + [assessmentSubmissionId], + [assessmentSubmissionsRows[2]] + ); + mockQuery( + 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?', + [assessmentSubmissionId], + [assessmentResponsesRows[1]] + ); + + expect( + await updateAssessmentSubmission(assessmentSubmissions[2], false) + ).toEqual(assessmentSubmissions[7]); + }); + + it('should not allow a participant to update grading information for themselves', async () => { + const expectedNow = DateTime.utc(2023, 2, 9, 12, 5, 0); + Settings.now = () => expectedNow.toMillis(); + + mockQuery( + 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?', + [assessmentSubmissionId], + [assessmentSubmissionsRows[3]] + ); + mockQuery( + 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?', + [assessmentSubmissionId], + [assessmentResponsesRows[2]] + ); + + expect( + await updateAssessmentSubmission(assessmentSubmissions[13], false) + ).toEqual(assessmentSubmissions[9]); + }); + it('should update an existing in-progress assessment submission by a participant with a changed response for FR response', async () => { + const expectedNow = DateTime.utc(2023, 2, 9, 12, 5, 0); + Settings.now = () => expectedNow.toMillis(); + + mockQuery( + 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?', + [assessmentSubmissionId], + [assessmentSubmissionsRows[1]] + ); + mockQuery( + 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?', + [assessmentSubmissionId], + [assessmentResponsesRows[4]] + ); + mockQuery( + 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?', + [programAssessments[0].id], + [programAssessmentsRows[0]] + ); + mockQuery( + 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?', + [programAssessments[0].program_id], + [programsRows[0]] + ); + mockQuery( + 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?', + [curriculumAssessmentId], + [curriculumAssessmentsRows[0]] + ); + mockQuery( + 'select `activity_types`.`title` from `activity_types` inner join `activities` on `activities`.`activity_type_id` = `activity_types`.`id` where `activities`.`id` = ?', + [curriculumAssessmentsRows[0].activity_id], + [ + { + title: curriculumAssessments[4].assessment_type, + }, + ] + ); + mockQuery( + 'update `assessment_responses` set `response` = ? where `id` = ?', + [assessmentResponsesRows[5].response, assessmentResponsesRows[5].id], + 1 + ); + mockQuery( + 'select `id` from `assessment_submission_states` where `title` = ?', + ['In Progress'], + [{ id: 4 }] + ); + mockQuery( + 'update `assessment_submissions` set `assessment_submission_state_id` = ? where `id` = ?', + [4, assessmentSubmissions[18].id], + 1 + ); + + expect( + await updateAssessmentSubmission(assessmentSubmissions[3], false) + ).toEqual(assessmentSubmissions[3]); + }); +}); + +describe('updateCurriculumAssessment', () => { + it('should update a curriculum assessment with existing questions and updated answers', async () => { + mockQuery( + 'select `assessment_questions`.`id`, `assessment_questions`.`title`, `description`, `assessment_question_types`.`title` as `question_type`, `correct_answer_id`, `max_score`, `sort_order` from `assessment_questions` inner join `assessment_question_types` on `assessment_questions`.`question_type_id` = `assessment_question_types`.`id` where `assessment_questions`.`assessment_id` = ? order by `sort_order` asc', + [curriculumAssessments[3].id], + [assessmentQuestionsRows[0]] + ); + + mockQuery( + 'select `id`, `question_id`, `title`, `description`, `sort_order` from `assessment_answers` where `question_id` = ? order by `sort_order` asc', + [assessmentQuestionsRows[0].id], + [assessmentAnswersRows[0]] + ); + + mockQuery( + 'update `assessment_answers` set `title` = ?, `description` = ?, `sort_order` = ? where `id` = ?', + [ + assessmentAnswersRows[2].title, + assessmentAnswersRows[2].description, + assessmentAnswersRows[2].sort_order, + assessmentAnswersRows[2].id, + ], + [1] + ); + + mockQuery( + 'update `assessment_questions` set `title` = ?, `description` = ?, `correct_answer_id` = ?, `max_score` = ?, `sort_order` = ? where `id` = ?', + [ + assessmentQuestionsRows[0].title, + assessmentQuestionsRows[0].description, + assessmentQuestionsRows[0].correct_answer_id, + assessmentQuestionsRows[0].max_score, + assessmentQuestionsRows[0].sort_order, + assessmentQuestionsRows[0].id, + ], + [1] + ); + + mockQuery( + 'update `curriculum_assessments` set `title` = ?, `description` = ?, `max_score` = ?, `max_num_submissions` = ?, `time_limit` = ?, `activity_id` = ? where `id` = ?', + [ + curriculumAssessmentsRows[0].title, + curriculumAssessmentsRows[0].description, + curriculumAssessmentsRows[0].max_score, + curriculumAssessmentsRows[0].max_num_submissions, + curriculumAssessmentsRows[0].time_limit, + curriculumAssessmentsRows[0].activity_id, + curriculumAssessmentsRows[0].id, + ], + [1] + ); + + expect(await updateCurriculumAssessment(curriculumAssessments[12])).toEqual( + curriculumAssessments[12] + ); + }); + + it('should update a curriculum assessment with new questions', async () => { + mockQuery( + 'select `assessment_questions`.`id`, `assessment_questions`.`title`, `description`, `assessment_question_types`.`title` as `question_type`, `correct_answer_id`, `max_score`, `sort_order` from `assessment_questions` inner join `assessment_question_types` on `assessment_questions`.`question_type_id` = `assessment_question_types`.`id` where `assessment_questions`.`assessment_id` = ? order by `sort_order` asc', + [curriculumAssessments[3].id], + [] + ); + + mockQuery( + 'insert into `assessment_questions` (`assessment_id`, `description`, `max_score`, `question_type_id`, `sort_order`, `title`) values (?, ?, ?, ?, ?, ?)', + [ + curriculumAssessmentId, + questions[4].description, + questions[4].max_score, + 1, + questions[4].sort_order, + questions[4].title, + ], + [singleChoiceQuestionId] + ); + + mockQuery( + 'insert into `assessment_answers` (`description`, `question_id`, `sort_order`, `title`) values (?, ?, ?, ?)', + [ + answers[4].description, + singleChoiceQuestionId, + answers[4].sort_order, + answers[4].title, + ], + [singleChoiceAnswerId] + ); + + mockQuery( + 'update `assessment_questions` set `correct_answer_id` = ? where `id` = ?', + [assessmentQuestionsRows[0].correct_answer_id, singleChoiceQuestionId], + [1] + ); + + mockQuery( + 'update `curriculum_assessments` set `title` = ?, `description` = ?, `max_score` = ?, `max_num_submissions` = ?, `time_limit` = ?, `activity_id` = ? where `id` = ?', + [ + curriculumAssessmentsRows[0].title, + curriculumAssessmentsRows[0].description, + curriculumAssessmentsRows[0].max_score, + curriculumAssessmentsRows[0].max_num_submissions, + curriculumAssessmentsRows[0].time_limit, + curriculumAssessmentsRows[0].activity_id, + curriculumAssessmentsRows[0].id, + ], + [1] + ); + + expect(await updateCurriculumAssessment(curriculumAssessments[9])).toEqual( + curriculumAssessments[3] + ); + }); + + it('should update a curriculum assessment with new question and delete old questions', async () => { + mockQuery( + 'select `assessment_questions`.`id`, `assessment_questions`.`title`, `description`, `assessment_question_types`.`title` as `question_type`, `correct_answer_id`, `max_score`, `sort_order` from `assessment_questions` inner join `assessment_question_types` on `assessment_questions`.`question_type_id` = `assessment_question_types`.`id` where `assessment_questions`.`assessment_id` = ? order by `sort_order` asc', + [curriculumAssessments[4].id], + [assessmentQuestionsRows[1]] + ); + + mockQuery( + 'delete from `assessment_questions` where `id` = ?', + [assessmentQuestionsRows[1].id], + [1] + ); + + mockQuery( + 'insert into `assessment_questions` (`assessment_id`, `description`, `max_score`, `question_type_id`, `sort_order`, `title`) values (?, ?, ?, ?, ?, ?)', + [ + curriculumAssessmentId, + questions[4].description, + questions[4].max_score, + 1, + questions[4].sort_order, + questions[4].title, + ], + [singleChoiceQuestionId] + ); + + mockQuery( + 'insert into `assessment_answers` (`description`, `question_id`, `sort_order`, `title`) values (?, ?, ?, ?)', + [ + answers[4].description, + singleChoiceQuestionId, + answers[4].sort_order, + answers[4].title, + ], + [singleChoiceAnswerId] + ); + + mockQuery( + 'update `assessment_questions` set `correct_answer_id` = ? where `id` = ?', + [assessmentQuestionsRows[0].correct_answer_id, singleChoiceQuestionId], + [1] + ); + + mockQuery( + 'update `curriculum_assessments` set `title` = ?, `description` = ?, `max_score` = ?, `max_num_submissions` = ?, `time_limit` = ?, `activity_id` = ? where `id` = ?', + [ + curriculumAssessmentsRows[0].title, + curriculumAssessmentsRows[0].description, + curriculumAssessmentsRows[0].max_score, + curriculumAssessmentsRows[0].max_num_submissions, + curriculumAssessmentsRows[0].time_limit, + curriculumAssessmentsRows[0].activity_id, + curriculumAssessmentsRows[0].id, + ], + [1] + ); + + expect(await updateCurriculumAssessment(curriculumAssessments[10])).toEqual( + curriculumAssessments[3] + ); + }); + + it('should update a curriculum assessment with new answer and delete existing answer', async () => { + mockQuery( + 'select `assessment_questions`.`id`, `assessment_questions`.`title`, `description`, `assessment_question_types`.`title` as `question_type`, `correct_answer_id`, `max_score`, `sort_order` from `assessment_questions` inner join `assessment_question_types` on `assessment_questions`.`question_type_id` = `assessment_question_types`.`id` where `assessment_questions`.`assessment_id` = ? order by `sort_order` asc', + [curriculumAssessments[3].id], + [assessmentQuestionsRows[0]] + ); + + mockQuery( + 'select `id`, `question_id`, `title`, `description`, `sort_order` from `assessment_answers` where `question_id` = ? order by `sort_order` asc', + [assessmentQuestionsRows[0].id], + [assessmentAnswersRows[0]] + ); + + mockQuery( + 'delete from `assessment_answers` where `id` = ?', + [assessmentAnswersRows[0].id], + [1] + ); + + mockQuery( + 'insert into `assessment_answers` (`description`, `question_id`, `sort_order`, `title`) values (?, ?, ?, ?)', + [ + answers[4].description, + singleChoiceQuestionId, + answers[4].sort_order, + answers[4].title, + ], + [singleChoiceAnswerId] + ); + + mockQuery( + 'update `assessment_questions` set `title` = ?, `description` = ?, `correct_answer_id` = ?, `max_score` = ?, `sort_order` = ? where `id` = ?', + [ + assessmentQuestionsRows[0].title, + assessmentQuestionsRows[0].description, + singleChoiceAnswerId, + assessmentQuestionsRows[0].max_score, + assessmentQuestionsRows[0].sort_order, + assessmentQuestionsRows[0].id, + ], + [1] + ); + + mockQuery( + 'update `curriculum_assessments` set `title` = ?, `description` = ?, `max_score` = ?, `max_num_submissions` = ?, `time_limit` = ?, `activity_id` = ? where `id` = ?', + [ + curriculumAssessmentsRows[0].title, + curriculumAssessmentsRows[0].description, + curriculumAssessmentsRows[0].max_score, + curriculumAssessmentsRows[0].max_num_submissions, + curriculumAssessmentsRows[0].time_limit, + curriculumAssessmentsRows[0].activity_id, + curriculumAssessmentsRows[0].id, + ], + [1] + ); + + expect(await updateCurriculumAssessment(curriculumAssessments[11])).toEqual( + curriculumAssessments[3] + ); + }); +}); + +describe('updateProgramAssessment', () => { + it('should return update for an existing program assessment ID', async () => { + mockQuery( + 'update `program_assessments` set `available_after` = ?, `due_date` = ? where `id` = ?', + [ + programAssessmentsRows[3].available_after, + programAssessmentsRows[3].due_date, + programAssessmentsRows[3].id, + ], + [] + ); + + expect(await updateProgramAssessment(programAssessmentsRows[3])).toEqual( + programAssessmentsRows[3] + ); + }); +}); diff --git a/api/src/services/__tests__/questionnairesService.ts b/api/src/services/__tests__/questionnairesService.ts index 5879d4f5..9305e72f 100644 --- a/api/src/services/__tests__/questionnairesService.ts +++ b/api/src/services/__tests__/questionnairesService.ts @@ -14,17 +14,17 @@ describe('questionnairesService', () => { }; const options = [{ id: 3, label: 'option label', prompt_id: promptId }]; mockQuery( - 'select `id` from `questionnaires` where `id` = ?', + 'select `id` from `surveys` where `id` = ?', [questionnaireId], [questionnaire] ); mockQuery( - 'select `id`, `label`, `query_text` from `prompts` where `questionnaire_id` = ? order by `sort_order` asc', + 'select `id`, `label`, `query_text` from `survey_questions` where `questionnaire_id` = ? order by `sort_order` asc', [questionnaireId], [prompt] ); mockQuery( - 'select `id`, `label`, `prompt_id` from `options` where `prompt_id` in (?) order by `prompt_id` asc', + 'select `id`, `label`, `prompt_id` from `survey_answers` where `prompt_id` in (?) order by `prompt_id` asc', [promptId], options ); @@ -37,7 +37,7 @@ describe('questionnairesService', () => { it('should return null if no questionnaire was found with the given id', async () => { const questionnaireId = 1; mockQuery( - 'select `id` from `questionnaires` where `id` = ?', + 'select `id` from `surveys` where `id` = ?', [questionnaireId], [] ); diff --git a/api/src/services/__tests__/reflectionsService.ts b/api/src/services/__tests__/reflectionsService.ts index 8a10bdc4..321e77aa 100644 --- a/api/src/services/__tests__/reflectionsService.ts +++ b/api/src/services/__tests__/reflectionsService.ts @@ -58,7 +58,7 @@ describe('reflectionsService', () => { journalEntries ); mockQuery( - 'select `responses`.`id` as `id`, `reflection_id` as `reflection_id`, `option_id` as `option_id`, `options`.`label` as `option_label`, `prompt_id` as `prompt_id`, `prompts`.`label` as `prompt_label` from `responses` inner join `options` on `responses`.`option_id` = `options`.`id` inner join `prompts` on `options`.`prompt_id` = `prompts`.`id` where `reflection_id` in (?) order by `prompts`.`sort_order` asc', + 'select `responses`.`id` as `id`, `reflection_id` as `reflection_id`, `option_id` as `option_id`, `survey_answers`.`label` as `option_label`, `prompt_id` as `prompt_id`, `survey_questions`.`label` as `prompt_label` from `responses` inner join `survey_answers` on `responses`.`option_id` = `survey_answers`.`id` inner join `survey_questions` on `survey_answers`.`prompt_id` = `survey_questions`.`id` where `reflection_id` in (?) order by `survey_questions`.`sort_order` asc', [reflectionId], responses ); diff --git a/api/src/services/assessmentsService.ts b/api/src/services/assessmentsService.ts new file mode 100644 index 00000000..20e226b9 --- /dev/null +++ b/api/src/services/assessmentsService.ts @@ -0,0 +1,1769 @@ +import { DateTime } from 'luxon'; +import { + Answer, + AssessmentResponse, + AssessmentSubmission, + CurriculumAssessment, + FacilitatorAssessmentSubmissionsSummary, + ParticipantAssessmentSubmissionsSummary, + ProgramAssessment, + Question, +} from '../models'; +import db from './db'; +import { findProgram, listProgramsForCurriculum } from './programsService'; + +/** + * Determines whether or not a specific program assessment submission has + * expired and is no longer allowed to be updated by the participant. + * + * @param {AssessmentSubmission} assessmentSubmission - The assessment + * submission to check for expiration, passed as an AssessmentSubmission + * object. + * @returns {Promise} If true, a participant should be prevented from + * submitting any updates to their program assessment submission, other than + * to mark it as "Expired" instead of "Opened" or "In Progress". + */ +const assessmentSubmissionExpired = async ( + assessmentSubmission: AssessmentSubmission +): Promise => { + const programAssessment = await findProgramAssessment( + assessmentSubmission.assessment_id + ); + const curriculumAssessment = await getCurriculumAssessment( + programAssessment.assessment_id, + false, + false + ); + + if (DateTime.now() > DateTime.fromISO(programAssessment.due_date)) { + return true; + } + + if ( + curriculumAssessment.time_limit && + typeof curriculumAssessment.time_limit === 'number' && + curriculumAssessment.time_limit > 0 + ) { + const endTime = DateTime.fromISO(assessmentSubmission.opened_at).plus({ + minutes: curriculumAssessment.time_limit, + }); + if (DateTime.now() >= endTime) { + return true; + } + } + + return false; +}; + +/** + * Calculates the total number of participants in a program that have started or + * completed *any* submission for a given program assessment. + * + * @param {number} programAssessmentId - The row ID of the program_assessments + * table for a given program assessment. + * @returns {Promise} The number of program participants with one or + * more submissions for that program assessment. + */ +const calculateNumParticipantsWithSubmissions = async ( + programAssessmentId: number +): Promise => { + const [numParticipantsWithSubmissions] = await db('assessment_submissions') + .where('assessment_id', programAssessmentId) + .countDistinct({ count: 'principal_id' }); + + return numParticipantsWithSubmissions.count; +}; + +/** + * Calculates the total number of participants enrolled in a program, excluding + * any program facilitators. + * + * @param {number} programId - The row ID of the programs table for a given + * program. + * @returns {Promise} The number of program participants in that + * program. + */ +const calculateNumProgramParticipants = async ( + programId: number +): Promise => { + const [matchingProgramParticipantsResults] = await db('program_participants') + .where('program_id', programId) + .andWhere('role_id', 2) + .count({ count: 'id' }); + + return matchingProgramParticipantsResults.count; +}; + +/** + * Calculates the total number of assessment submissions that have yet to be + * graded for a given program assessment. + * + * @param {number} programAssessmentId - The row ID of the program_assessments + * table for a given program assessment. + * @returns {Promise} The number of assessment submissions that have + * been submitted but not graded. + */ +const calculateNumUngradedSubmissions = async ( + programAssessmentId: number +): Promise => { + const [numUngradedSubmissions] = await db('assessment_submissions') + .where('assessment_id', programAssessmentId) + .andWhere('score', null) + .count({ count: 'id' }); + + return numUngradedSubmissions.count; +}; + +/** + * Inserts a question for an existing curriculum assessment into the + * assessment_questions table. + * + * @param {number} curriculumAssessmentId - The curriculum assessment to which + * we are adding this question. + * @param {Question} question - An object containing the question, its metadata, + * and any possible answers. + * @returns {Promise} The updated Question object that was handed to + * us but with updated row IDs for the question and all answers given to us. + */ +const createAssessmentQuestion = async ( + curriculumAssessmentId: number, + question: Question +): Promise => { + const [insertedAssessmentQuestionId] = await db( + 'assessment_questions' + ).insert({ + assessment_id: curriculumAssessmentId, + title: question.title, + description: question.description, + question_type_id: question.question_type === 'single choice' ? 1 : 2, + max_score: question.max_score, + sort_order: question.sort_order, + }); + + const insertedAnswers: Answer[] = []; + let correctAnswerId; + + for (const assessmentAnswer of question.answers) { + const insertedAnswer = await createAssessmentQuestionAnswer( + insertedAssessmentQuestionId, + assessmentAnswer + ); + + if (question.question_type === 'single choice') { + if (insertedAnswer.correct_answer === true) { + correctAnswerId = insertedAnswer.id; + } + } else if (question.question_type === 'free response') { + correctAnswerId = insertedAnswer.id; + } + insertedAnswers.push(insertedAnswer); + } + + const updatedAssessmentQuestion = { + ...question, + id: insertedAssessmentQuestionId, + answers: insertedAnswers, + }; + + if (correctAnswerId) { + await db('assessment_questions') + .update('correct_answer_id', correctAnswerId) + .where('id', insertedAssessmentQuestionId); + updatedAssessmentQuestion.correct_answer_id = correctAnswerId; + } + + return updatedAssessmentQuestion; +}; + +/** + * Inserts an answer for an existing curriculum assessment question into the + * assessment_answers table. + * + * @param {number} questionId - The row ID of the assessment_questions table for + * a given question. + * @param {Answer} answer - An object containing an answer option and its + * metadata. + * @returns {Promise} The updated Answer object that was handed to us + * but with row ID specified. + */ +const createAssessmentQuestionAnswer = async ( + questionId: number, + answer: Answer +): Promise => { + const [insertedAssessmentAnswersId] = await db('assessment_answers').insert({ + question_id: questionId, + title: answer.title, + description: answer.description, + sort_order: answer.sort_order, + }); + + const updatedAssessmentAnswer = { + ...answer, + question_id: questionId, + id: insertedAssessmentAnswersId, + }; + + return updatedAssessmentAnswer; +}; + +/** + * Inserts a response for a user for a given curriculum assessment question into + * the assessment_responses table. It does not check if the question relates to + * the curriculum assessment for which the submission relates, nor does it check + * to see if the answer relates to the question (for single choice questions). + * + * @param {AssessmentResponse} assessmentResponse - An object containing the + * assessment response data. + * @returns {Promise} The updated AssessmentResponse object + * that was handed to us but with row ID specified. + */ +const createSubmissionResponse = async ( + assessmentResponse: AssessmentResponse +): Promise => { + const [newSubmissionResponseId] = await db('assessment_responses').insert({ + assessment_id: assessmentResponse.assessment_id, + submission_id: assessmentResponse.submission_id, + question_id: assessmentResponse.question_id, + answer_id: assessmentResponse.answer_id, + response: assessmentResponse.response_text, + }); + + const newSubmissionResponse: AssessmentResponse = { + id: newSubmissionResponseId, + assessment_id: assessmentResponse.assessment_id, + submission_id: assessmentResponse.submission_id, + question_id: assessmentResponse.question_id, + }; + + if (assessmentResponse.answer_id) { + newSubmissionResponse.answer_id = assessmentResponse.answer_id; + } else if (assessmentResponse.response_text) { + newSubmissionResponse.response_text = assessmentResponse.response_text; + } + + return newSubmissionResponse; +}; + +/** + * Removes an existing question from a curriculum assessment without deleting + * the entire assessment. + * + * @param {number} questionId - The row ID of the assessment_questions table for + * a given question. + * @returns {Promise} Returns nothing if the deletion was successful. + */ +const deleteAssessmentQuestion = async (questionId: number): Promise => { + return db('assessment_questions').where('id', questionId).delete(); +}; + +/** + * Removes an existing answer option from a curriculum assessment question + * without deleting the question. + * + * @param {number} answerId - The row ID of the assessment_answers table for a + * given answer. + * @returns {Promise} Returns nothing if the deletion was successful. + */ +const deleteAssessmentQuestionAnswer = async ( + answerId: number +): Promise => { + return db('assessment_answers').where('id', answerId).delete(); +}; + +/** + * Lists all possible answer options for a given curriculum assessment question. + * If specified, will also return metadata indicating which answer option is + * correct. + * + * @param {number} questionId - The row ID of the assessment_questions table for + * a given question. + * @returns {Promise} An array of Answer options, including or + * omitting the correct answer metadata as specified. + */ +const listAssessmentQuestionAnswers = async ( + questionId: number +): Promise => { + const assessmentAnswersList = await db('assessment_answers') + .select('id', 'question_id', 'title', 'description', 'sort_order') + .where('question_id', questionId) + .orderBy('sort_order', 'asc'); + + return assessmentAnswersList; +}; + +/** + * Lists all questions of a given curriculum assessment. Based on specified + * boolean parameter, will also return metadata indicating the correct answer + * option. Note that for free response questions, this function will not return + * any answers unless correctAnswersIncluded is set to true. + * + * @param {number} curriculumAssessmentId - The row ID of the + * curriculum_assessments table for a given curriculum assessment. + * @param {boolean} [answersIncluded] - Optional specifier to determine whether + * or not the answers for the questions should be included or removed from the + * return value. + * @param {boolean} [correctAnswersIncluded] - Optional specifier to determine + * whether or not the correct answer information should be included or removed + * from the return value. + * @returns {Promise} An array of Question objects, including or + * omitting the correct answer metadata as specified. + */ +const listAssessmentQuestions = async ( + curriculumAssessmentId: number, + answersIncluded?: boolean, + correctAnswersIncluded?: boolean +): Promise => { + const matchingAssessmentQuestionsRows = await db('assessment_questions') + .join( + 'assessment_question_types', + 'assessment_questions.question_type_id', + 'assessment_question_types.id' + ) + .select( + 'assessment_questions.id', + 'assessment_questions.title', + 'description', + 'assessment_question_types.title as question_type', + 'correct_answer_id', + 'max_score', + 'sort_order' + ) + .where('assessment_questions.assessment_id', curriculumAssessmentId) + .orderBy('sort_order', 'asc'); + + if (matchingAssessmentQuestionsRows.length === 0) { + return null; + } + + const assessmentQuestions: Question[] = []; + + for (const assessmentQuestionsRow of matchingAssessmentQuestionsRows) { + const assessmentQuestion: Question = { + id: assessmentQuestionsRow.id, + assessment_id: curriculumAssessmentId, + title: assessmentQuestionsRow.title, + description: assessmentQuestionsRow.description, + question_type: assessmentQuestionsRow.question_type, + max_score: assessmentQuestionsRow.max_score, + sort_order: assessmentQuestionsRow.sort_order, + }; + + if (answersIncluded && answersIncluded === true) { + assessmentQuestion.answers = await listAssessmentQuestionAnswers( + assessmentQuestionsRow.id + ); + + if (correctAnswersIncluded && correctAnswersIncluded === true) { + assessmentQuestion.correct_answer_id = + assessmentQuestionsRow.correct_answer_id; + const correctAnswer = assessmentQuestion.answers.find( + answer => answer.id === assessmentQuestionsRow.correct_answer_id + ); + if (correctAnswer) { + correctAnswer.correct_answer = true; + } + } + } + + assessmentQuestions.push(assessmentQuestion); + } + + return assessmentQuestions; +}; + +/** + * Lists all responses from a given assessment submission by a program + * participant. Based on specified boolean parameter, will also include the + * score and any grader response as well. + * + * @param {number} submissionId - The row ID of the assessment_submissions table + * for a given program assessment submission. + * @param {boolean} [gradingsIncluded] - Optional specifier to determine whether + * or not the grading information (score, grader response) should be included + * or removed from the return value. + * @returns {Promise} An array of AssessmentResponse + * objects, including or omitting the grading information as specified. + */ +const listSubmissionResponses = async ( + submissionId: number, + gradingsIncluded?: boolean +): Promise => { + const matchingAssessmentResponsesRows = await db('assessment_responses') + .select( + 'id', + 'assessment_id', + 'question_id', + 'answer_id', + 'response', + 'score', + 'grader_response' + ) + .where('submission_id', submissionId); + + if (matchingAssessmentResponsesRows.length === 0) { + return null; + } + + const assessmentResponses: AssessmentResponse[] = + matchingAssessmentResponsesRows.map(assessmentResponsesRow => { + const assessmentResponse: AssessmentResponse = { + id: assessmentResponsesRow.id, + assessment_id: assessmentResponsesRow.assessment_id, + submission_id: submissionId, + question_id: assessmentResponsesRow.question_id, + }; + + if ( + assessmentResponsesRow.answer_id === null && + assessmentResponsesRow.response !== null + ) { + assessmentResponse.response_text = assessmentResponsesRow.response; + } else if (assessmentResponsesRow.answer_id !== null) { + assessmentResponse.answer_id = assessmentResponsesRow.answer_id; + } + + if (gradingsIncluded && gradingsIncluded === true) { + assessmentResponse.score = assessmentResponsesRow.score; + assessmentResponse.grader_response = + assessmentResponsesRow.grader_response; + } + + return assessmentResponse; + }); + + return assessmentResponses; +}; + +/** + * Updates an existing curriculum assessment question with new answer options or + * new metadata. + * + * @param {Question} question - The Question object for a given curriculum + * assessment question with updated information. + * @returns {Promise} The updated Question object, including created + * or updated Answer objects, if any. + */ +const updateAssessmentQuestion = async ( + question: Question +): Promise => { + const existingAnswers = await listAssessmentQuestionAnswers(question.id); + + const updatedAnswers: Answer[] = []; + const newAnswers: Answer[] = []; + + const updatedQuestion = { + ...question, + }; + + let correctAnswerId = question.correct_answer_id; + + if ( + question.answers && + Array.isArray(question.answers) && + question.answers.length > 0 + ) { + for (const answer of question.answers) { + if (answer.id && typeof answer.id === 'number' && answer.id > 0) { + if ( + existingAnswers && + Array.isArray(existingAnswers) && + existingAnswers.length > 0 + ) { + const eqIndex = existingAnswers.findIndex( + existingAnswer => existingAnswer.id === answer.id + ); + existingAnswers.splice(eqIndex, 1); + } + updatedAnswers.push(answer); + + if (answer.correct_answer && answer.correct_answer === true) { + correctAnswerId = answer.id; + } + } else { + newAnswers.push(answer); + } + } + } + + if ( + existingAnswers && + Array.isArray(existingAnswers) && + existingAnswers.length > 0 + ) { + for (const deletedAnswer of existingAnswers) { + await deleteAssessmentQuestionAnswer(deletedAnswer.id); + } + } + + const newAnswersList: Answer[] = []; + + for (const newAnswer of newAnswers) { + const newAnswerInserted = await createAssessmentQuestionAnswer( + question.id, + newAnswer + ); + + if (newAnswer.correct_answer && newAnswer.correct_answer === true) { + correctAnswerId = newAnswerInserted.id; + } + + newAnswersList.push(newAnswerInserted); + } + + for (const updatedAnswer of updatedAnswers) { + newAnswersList.push(await updateAssessmentQuestionAnswer(updatedAnswer)); + } + + updatedQuestion.correct_answer_id = correctAnswerId; + + await db('assessment_questions') + .update({ + title: question.title, + description: question.description, + correct_answer_id: correctAnswerId, + max_score: question.max_score, + sort_order: question.sort_order, + }) + .where('id', question.id); + + updatedQuestion.answers = newAnswersList; + + return updatedQuestion; +}; + +/** + * Updates an existing answer option for a curriculum assessment question with + * new metadata. + * + * @param {Answer} answer - The Answer object for a given curriculum assessment + * question answer option with updated information. + * @returns {Promise} The updated Answer object. + */ +const updateAssessmentQuestionAnswer = async ( + answer: Answer +): Promise => { + await db('assessment_answers') + .update({ + title: answer.title, + description: answer.description, + sort_order: answer.sort_order, + }) + .where('id', answer.id); + return answer; +}; + +/** + * Updates an existing assessment submission response with updated metadata. It + * does not check if the question relates to the curriculum assessment for which + * the submission relates, nor does it check to see if the answer relates to the + * question (for single choice questions). + * + * @param {AssessmentResponse} assessmentResponse - The AssessmentResponse + * object for a given program assessment submission response with updated + * information. + * @param {boolean} [facilitatorGrading] - Optional specifier for when the + * program facilitator is the one updating a program assessment submission + * response, so that a facilitator is allowed to modify the score and grader + * response instead of a participant who is allowed to update their answer. + * @returns {Promise} The updated AssessmentResponse object. + */ +const updateSubmissionResponse = async ( + assessmentResponse: AssessmentResponse, + facilitatorGrading?: boolean +): Promise => { + if (facilitatorGrading && facilitatorGrading === true) { + await db('assessment_responses') + .update({ + score: assessmentResponse.score, + grader_response: assessmentResponse.grader_response, + }) + .where('id', assessmentResponse.id); + } else { + await db('assessment_responses') + .update({ + answer_id: assessmentResponse.answer_id, + response: assessmentResponse.response_text, + }) + .where('id', assessmentResponse.id); + } + + return assessmentResponse; +}; + +/** + * Gathers the relevant information for constructing a + * FacilitatorAssessmentSubmissionsSummary for a given program assessment. + * + * @param {number} programAssessment - The program assessment for which we're + * constructing a summary. + * @returns {Promise} The program + * assessment submissions summary information for use by a program + * facilitator. + */ +export const constructFacilitatorAssessmentSummary = async ( + programAssessment: ProgramAssessment +): Promise => { + const numParticipantsWithSubmissions = + await calculateNumParticipantsWithSubmissions(programAssessment.id); + const numProgramParticipants = await calculateNumProgramParticipants( + programAssessment.program_id + ); + const numUngradedSubmissions = await calculateNumUngradedSubmissions( + programAssessment.id + ); + + const facilitatorAssessmentSummary: FacilitatorAssessmentSubmissionsSummary = + { + num_participants_with_submissions: numParticipantsWithSubmissions, + num_program_participants: numProgramParticipants, + num_ungraded_submissions: numUngradedSubmissions, + }; + + return facilitatorAssessmentSummary; +}; + +/** + * Gathers the relevant information for constructing a + * ParticipantAssessmentSubmissionsSummary for a given participant principal ID + * and a given program assessment. + * + * @param {number} participantPrincipalId - The row ID of the principals table + * that corresponds with a given program participant. + * @param {ProgramAssessment} programAssessment - The program assessment for + * which we're constructing a summary. + * @returns {Promise} The program + * assessment submissions summary information for use by a program + * participant. + */ +export const constructParticipantAssessmentSummary = async ( + participantPrincipalId: number, + programAssessment: ProgramAssessment +): Promise => { + const assessmentActiveDate = DateTime.fromISO( + programAssessment.available_after + ); + const assessmentDueDate = DateTime.fromISO(programAssessment.due_date); + + let highestState; + + const highestStateFromDB = await db('assessment_submissions') + .select('assessment_submission_states.title') + .join( + 'assessment_submission_states', + 'assessment_submission_states.id', + 'assessment_submissions.assessment_submission_state_id' + ) + .where('assessment_submissions.principal_id', participantPrincipalId) + .andWhere('assessment_submissions.assessment_id', programAssessment.id) + .orderBy('assessment_submissions.assessment_submission_state_id', 'desc') + .limit(1); + + if (highestStateFromDB.length === 0) { + if ( + DateTime.now() >= assessmentActiveDate && + DateTime.now() < assessmentDueDate + ) { + highestState = 'Active'; + } else if (DateTime.now() < assessmentActiveDate) { + highestState = 'Inactive'; + } else if (DateTime.now() >= assessmentDueDate) { + highestState = 'Expired'; + } + } else { + highestState = highestStateFromDB[0].title; + } + + let mostRecentSubmittedDate; + const mostRecentSubmittedDateFromDB = await db('assessment_submissions') + .select('submitted_at') + .where('principal_id', participantPrincipalId) + .andWhere('assessment_id', programAssessment.id) + .orderBy('submitted_at', 'desc') + .limit(1); + + if (mostRecentSubmittedDateFromDB.length === 0) { + mostRecentSubmittedDate = null; + } else { + mostRecentSubmittedDate = DateTime.fromSQL( + mostRecentSubmittedDateFromDB[0].submitted_at, + { zone: 'utc' } + ).toISO(); + } + + const totalNumSubmissions = await listParticipantProgramAssessmentSubmissions( + participantPrincipalId, + programAssessment.id + ); + + let highestScore; + const highestScoreFromDB = await db('assessment_submissions') + .select('score') + .where('principal_id', participantPrincipalId) + .andWhere('assessment_id', programAssessment.id) + .orderBy('score', 'desc') + .limit(1); + + if (highestScoreFromDB.length === 0) { + highestScore = null; + } else { + highestScore = highestScoreFromDB[0].score; + } + + const participantAssessmentSummary: ParticipantAssessmentSubmissionsSummary = + { + principal_id: participantPrincipalId, + highest_state: highestState, + total_num_submissions: totalNumSubmissions + ? totalNumSubmissions.length + : 0, + }; + + if (mostRecentSubmittedDate !== null) { + participantAssessmentSummary.most_recent_submitted_date = + mostRecentSubmittedDate; + } + + if (highestScore !== null) { + participantAssessmentSummary.highest_score = highestScore; + } + + return participantAssessmentSummary; +}; + +/** + * Begins a new program assessment submission for a program participant, if they + * have not exceeded the maximum number of allowed submissions for that + * assessment and no other assessment submissions are in progress by that + * program participant for that program assessment. + * + * @param {number} participantPrincipalId - The row ID of the principals table + * that corresponds with a given program participant. + * @param {number} programAssessmentId - The row ID of the program_assessments + * table for a given program assessment. + * @param {number} curriculumAssessmentId - The row ID of the + * curriculum_assessments table, to save us a database query. + * @returns {Promise} An AssessmentSubmission object + * constructed from the inserted row in the assessment_submissions table. + */ +export const createAssessmentSubmission = async ( + participantPrincipalId: number, + programAssessmentId: number, + curriculumAssessmentId: number +): Promise => { + const openedStateTitle = 'Opened'; + const [openedStateId] = await db('assessment_submission_states') + .select('id') + .where('title', openedStateTitle); + + const [newSubmissionId] = await db('assessment_submissions').insert({ + assessment_id: programAssessmentId, + principal_id: participantPrincipalId, + assessment_submission_state_id: openedStateId.id, + }); + + const assessmentQuestionIds = await db('assessment_questions') + .select('id') + .where({ assessment_id: curriculumAssessmentId }); + + const submissionResponses: AssessmentResponse[] = []; + + for (const question of assessmentQuestionIds) { + const response = await createSubmissionResponse({ + assessment_id: programAssessmentId, + submission_id: newSubmissionId, + question_id: question.id, + }); + + submissionResponses.push(response); + } + + const newSubmission: AssessmentSubmission = { + id: newSubmissionId, + assessment_id: programAssessmentId, + principal_id: participantPrincipalId, + assessment_submission_state: openedStateTitle, + opened_at: DateTime.now().toUTC().toISO(), + last_modified: DateTime.now().toUTC().toISO(), + responses: submissionResponses, + }; + + return newSubmission; +}; + +/** + * Creates a new curriculum assessment in the curriculum_assessments table, + * linked with a given curriculum activity. + * + * @param {CurriculumAssessment} curriculumAssessment - The CurriculumAssessment + * object for the new curriculum assessment data to be inserted. + * @returns {Promise} The updated CurriculumAssessment + * object that was handed to us, but with row ID specified. + */ +export const createCurriculumAssessment = async ( + curriculumAssessment: CurriculumAssessment +): Promise => { + const [insertedCurriculumAssessmentRowId] = await db( + 'curriculum_assessments' + ).insert({ + title: curriculumAssessment.title, + description: curriculumAssessment.description, + max_score: curriculumAssessment.max_score, + max_num_submissions: curriculumAssessment.max_num_submissions, + time_limit: curriculumAssessment.time_limit, + curriculum_id: curriculumAssessment.curriculum_id, + activity_id: curriculumAssessment.activity_id, + principal_id: curriculumAssessment.principal_id, + }); + + const insertedQuestions: Question[] = []; + + if (typeof curriculumAssessment.questions !== 'undefined') { + for (const assessmentQuestion of curriculumAssessment.questions) { + insertedQuestions.push( + await createAssessmentQuestion( + insertedCurriculumAssessmentRowId, + assessmentQuestion + ) + ); + } + } + + const updatedCurriculumAssessment: CurriculumAssessment = { + ...curriculumAssessment, + id: insertedCurriculumAssessmentRowId, + }; + + if (insertedQuestions.length > 0) { + updatedCurriculumAssessment.questions = insertedQuestions; + } + + return updatedCurriculumAssessment; +}; + +/** + * Creates a new program assessment in the program_assessments table, linked + * with a given curriculum assessment. + * + * @param {ProgramAssessment} programAssessment - The ProgramAssessment object + * for the new program assessment data to be inserted. + * @returns {Promise} The updated ProgramAssessment object + * that was handed to us, but with row ID specified. + */ +export const createProgramAssessment = async ( + programAssessment: ProgramAssessment +): Promise => { + const [insertedProgramAssessmentRowId] = await db( + 'program_assessments' + ).insert({ + program_id: programAssessment.program_id, + assessment_id: programAssessment.assessment_id, + available_after: programAssessment.available_after, + due_date: programAssessment.due_date, + }); + + const program = await findProgram(programAssessment.program_id); + + const updatedProgramAssessment: ProgramAssessment = { + ...programAssessment, + id: insertedProgramAssessmentRowId, + available_after: DateTime.fromSQL(programAssessment.available_after, { + zone: program.time_zone, + }).toISO(), + due_date: DateTime.fromSQL(programAssessment.due_date, { + zone: program.time_zone, + }).toISO(), + }; + + return updatedProgramAssessment; +}; + +/** + * Deletes a given curriculum assessment, all associated program assessments, + * and all associated questions and answers for a given curriculum assessment. + * This function fails to execute if there has ever been an assessment + * submission for the questions and answers in this curriculum assessment. + * + * @param {number} curriculumAssessmentId - The row ID of the + * curriculum_assessments table for a given curriculum assessment. + * @returns {Promise} Returns nothing if the deletion was successful. + */ +export const deleteCurriculumAssessment = async ( + curriculumAssessmentId: number +): Promise => { + return db('curriculum_assessments') + .where('id', curriculumAssessmentId) + .delete(); +}; + +/** + * Deletes a given program assessment, but leaves the curriculum assessment, its + * questions, and its answers intact. This function fails to execute if there + * has ever been an assessment submission for this program assessment by a + * program participant. + * + * @param {number} programAssessmentId - The row ID of the program_assessments + * table for a given program assessment. + * @returns {Promise} Returns nothing if the deletion was successful. + */ +export const deleteProgramAssessment = async ( + programAssessmentId: number +): Promise => { + return db('program_assessments').where('id', programAssessmentId).delete(); +}; + +/** + * For demonstration purposes only and should be removed from the shipping + * product. Enrolls a facilitator in a program so that the assessments list + * feature can be demonstrated. + * + * @param {number} principalId - The row ID of the principals table + * corresponding with the user to enroll as a facilitator in a program. + * @param {number} programId - The row ID of the programs table corresponding + * with the program into which the facilitator should be enrolled. + * @returns {Promise} Returns `true` if the user was successfully + * enrolled in the program as a facilitator or if the user was switched to the + * facilitator role; returns `false` if the user was already a facilitator in + * the program. + */ +export const enrollFacilitator = async ( + principalId: number, + programId: number +): Promise => { + const isEnrolled = await db('program_participants') + .where({ + principal_id: principalId, + program_id: programId, + }) + .select('role_id'); + + if (isEnrolled.length > 0) { + if (isEnrolled[0].role_id !== 1) { + await db('program_participants') + .where({ principal_id: principalId, program_id: programId }) + .update({ role_id: 1 }); + return true; + } + return false; + } else { + await db('program_participants').insert({ + principal_id: principalId, + program_id: programId, + role_id: 1, + }); + return true; + } +}; + +/** + * For demonstration purposes only and should be removed from the shipping + * product. Enrolls a participant in a program so that the assessments list + * feature can be demonstrated. + * + * @param {number} principalId - The row ID of the principals table + * corresponding with the user to enroll as a participant in a program. + * @param {number} programId - The row ID of the programs table corresponding + * with the program into which the participant should be enrolled. + * @returns {Promise} Returns `true` if the user was successfully + * enrolled in the program as a participant or if the user was switched to the + * participant role; returns `false` if the user was already a participant in + * the program. + */ +export const enrollParticipant = async ( + principalId: number, + programId: number +): Promise => { + const isEnrolled = await db('program_participants') + .where({ + principal_id: principalId, + program_id: programId, + }) + .select('role_id'); + + if (isEnrolled.length > 0) { + if (isEnrolled[0].role_id !== 2) { + await db('program_participants') + .where({ principal_id: principalId, program_id: programId }) + .update({ role_id: 2 }); + return true; + } + return false; + } else { + await db('program_participants').insert({ + principal_id: principalId, + program_id: programId, + role_id: 2, + }); + return true; + } +}; + +/** + * Finds a single program assessment by its row ID, if it exists in the + * program_assessments table. + * + * @param {number} programAssessmentId - The row ID of the program_assessments + * table for a given program assessment. + * @returns {Promise} The ProgramAssessment representation of + * that program assessment, or null if no matching program assessment was + * found. + */ +export const findProgramAssessment = async ( + programAssessmentId: number +): Promise => { + const matchingProgramAssessmentsRows = await db('program_assessments') + .select('program_id', 'assessment_id', 'available_after', 'due_date') + .where('id', programAssessmentId); + + if (matchingProgramAssessmentsRows.length === 0) { + return null; + } + + const [programAssessmentsRow] = matchingProgramAssessmentsRows; + + const program = await findProgram(programAssessmentsRow.program_id); + + const programAssessment: ProgramAssessment = { + id: programAssessmentId, + program_id: programAssessmentsRow.program_id, + assessment_id: programAssessmentsRow.assessment_id, + available_after: DateTime.fromSQL(programAssessmentsRow.available_after, { + zone: program.time_zone, + }).toISO(), + due_date: DateTime.fromSQL(programAssessmentsRow.due_date, { + zone: program.time_zone, + }).toISO(), + }; + + return programAssessment; +}; + +/** + * Finds a single program assessment submission by its row ID, if it exists in + * the assessment_submissions table. Optionally returns the submission's saved + * responses and the grading information for the submission and its responses. + * + * @param {number} assessmentSubmissionId - The row ID of the + * assessment_submissions table for a given program assessment submission. + * @param {boolean} [responsesIncluded] - Optional specifier to determine + * whether or not the assessment responses will be included in the returned + * object. + * @param {boolean} [gradingsIncluded] - Optional specifier to override the + * default grading information return behavior, such as if a program + * facilitator was retrieving the assessment submission. If this parameter is + * not specified, the default behavior will take over: the grading information + * will only be released if the assessment submission is in the "Graded" + * state. + * @returns {Promise} The AssessmentSubmission + * representation of that program assessment submission, or null if no + * matching program assessment submission was found. + */ +export const getAssessmentSubmission = async ( + assessmentSubmissionId: number, + responsesIncluded?: boolean, + gradingsIncluded?: boolean +): Promise => { + const matchingAssessmentSubmissionsRows = await db('assessment_submissions') + .join( + 'assessment_submission_states', + 'assessment_submissions.assessment_submission_state_id', + 'assessment_submission_states.id' + ) + .select( + 'assessment_submissions.assessment_id', + 'assessment_submissions.principal_id', + 'assessment_submission_states.title as assessment_submission_state', + 'assessment_submissions.score', + 'assessment_submissions.opened_at', + 'assessment_submissions.submitted_at', + 'assessment_submissions.updated_at' + ) + .where('assessment_submissions.id', assessmentSubmissionId); + + if (matchingAssessmentSubmissionsRows.length === 0) { + return null; + } + + const [assessmentSubmissionsRow] = matchingAssessmentSubmissionsRows; + + const assessmentSubmission: AssessmentSubmission = { + id: assessmentSubmissionId, + assessment_id: assessmentSubmissionsRow.assessment_id, + principal_id: assessmentSubmissionsRow.principal_id, + assessment_submission_state: + assessmentSubmissionsRow.assessment_submission_state, + opened_at: DateTime.fromSQL(assessmentSubmissionsRow.opened_at, { + zone: 'utc', + }).toISO(), + last_modified: DateTime.fromSQL(assessmentSubmissionsRow.updated_at, { + zone: 'utc', + }).toISO(), + }; + + if (assessmentSubmissionsRow.score !== null) { + assessmentSubmission.score = assessmentSubmissionsRow.score; + } + + if (assessmentSubmissionsRow.submitted_at !== null) { + assessmentSubmission.submitted_at = DateTime.fromSQL( + assessmentSubmissionsRow.submitted_at, + { + zone: 'utc', + } + ).toISO(); + } + + if (responsesIncluded) { + const assessmentResponses = await listSubmissionResponses( + assessmentSubmissionId, + gradingsIncluded + ); + if (assessmentResponses !== null) { + assessmentSubmission.responses = assessmentResponses; + } + } + return assessmentSubmission; +}; + +/** + * Finds a single curriculum assessment by its row ID, if it exists in the + * curriculum_assessments table. Optionally returns the questions and all answer + * options, such as when a participant is creating or viewing an assessment + * submission, and the questions and correct answers, such as when a participant + * is viewing a graded submission or a facilitator is grading a submission. + * + * @param {number} curriculumAssessmentId - The row ID of the + * curriculum_assessments table for a given curriculum assessment. + * @param {boolean} [questionsAndAllAnswersIncluded] - Optional specifier to + * determine whether or not the questions and all answer options will be + * included in the returned object. + * @param {boolean} [questionsAndCorrectAnswersIncluded] - Optional specifier to + * determine whether or not the correct answer information for the curriculum + * assessment questions will be included in the returned object. + * @returns {Promise} The CurriculumAssessment + * representation of that curriculum assessment, or null if no matching + * curriculum assessment was found. + */ +export const getCurriculumAssessment = async ( + curriculumAssessmentId: number, + questionsAndAllAnswersIncluded?: boolean, + questionsAndCorrectAnswersIncluded?: boolean +): Promise => { + const matchingCurriculumAssessmentRows = await db('curriculum_assessments') + .select( + 'curriculum_assessments.title', + 'curriculum_assessments.max_score', + 'curriculum_assessments.max_num_submissions', + 'curriculum_assessments.time_limit', + 'curriculum_assessments.curriculum_id', + 'curriculum_assessments.activity_id', + 'curriculum_assessments.principal_id' + ) + .join('activities', 'curriculum_assessments.curriculum_id', 'activities.id') + .where('curriculum_assessments.id', curriculumAssessmentId); + + if (matchingCurriculumAssessmentRows.length === 0) { + return null; + } + + const [matchingCurriculumAssessment] = matchingCurriculumAssessmentRows; + + const [assessmentType] = await db('activity_types') + .select('activity_types.title') + .join('activities', 'activities.activity_type_id', 'activity_types.id') + .where('activities.id', matchingCurriculumAssessment.activity_id); + + const curriculumAssessment: CurriculumAssessment = { + id: curriculumAssessmentId, + title: matchingCurriculumAssessment.title, + assessment_type: assessmentType.title, + description: matchingCurriculumAssessment.description, + max_score: matchingCurriculumAssessment.max_score, + max_num_submissions: matchingCurriculumAssessment.max_num_submissions, + time_limit: matchingCurriculumAssessment.time_limit, + curriculum_id: matchingCurriculumAssessment.curriculum_id, + activity_id: matchingCurriculumAssessment.activity_id, + principal_id: matchingCurriculumAssessment.principal_id, + }; + + if (questionsAndAllAnswersIncluded === true) { + curriculumAssessment.questions = await listAssessmentQuestions( + curriculumAssessmentId, + questionsAndAllAnswersIncluded, + questionsAndCorrectAnswersIncluded + ); + } + + return curriculumAssessment; +}; + +/** + * Retrieves the string representation of a principal's role for a given + * program: "Facilitator" for a program facilitator, "Participant" for a program + * participant, or null if not enrolled in the specified program. + * + * @param {number} principalId - The row ID of the principals table that + * corresponds with a given program member. + * @param {number} programId - The row ID of the programs table for a given + * program. + * @returns {Promise} The string value of a principal's role in a given + * program, or null if they are not enrolled as a participant or facilitating + * that program. + */ +export const getPrincipalProgramRole = async ( + principalId: number, + programId: number +): Promise => { + const matchingRoleRows = await db('program_participant_roles') + .select('program_participant_roles.title') + .join( + 'program_participants', + 'program_participant_roles.id', + 'program_participants.role_id' + ) + .where({ principal_id: principalId, program_id: programId }); + + if (matchingRoleRows.length === 0) { + return null; + } + + const [matchingRole] = matchingRoleRows; + + return matchingRole.title; +}; + +/** + * Lists all submissions by all program participants for a given program + * assessment, if any. Does not include responses for those submissions. + * + * @param {number} programAssessmentId - The row ID of the program_assessments + * table for a given program assessment. + * @returns {Promise} An array of AssessmentSubmission + * objects constructed from matching program assessment submissions, if any, + * not including their responses. + */ +export const listAllProgramAssessmentSubmissions = async ( + programAssessmentId: number +): Promise => { + const matchingAssessmentSubmissionsRows = await db('assessment_submissions') + .join( + 'assessment_submission_states', + 'assessment_submissions.assessment_submission_state_id', + 'assessment_submission_states.id' + ) + .select( + 'assessment_submissions.id', + 'assessment_submission_states.title as assessment_submission_state', + 'assessment_submissions.principal_id', + 'assessment_submissions.score', + 'assessment_submissions.opened_at', + 'assessment_submissions.submitted_at', + 'assessment_submissions.updated_at' + ) + .where('assessment_id', programAssessmentId); + + if (matchingAssessmentSubmissionsRows.length === 0) { + return null; + } + + const assessmentSubmissions: AssessmentSubmission[] = []; + + for (const assessmentSubmissionsRow of matchingAssessmentSubmissionsRows) { + const assessmentSubmission: AssessmentSubmission = { + id: assessmentSubmissionsRow.id, + assessment_id: programAssessmentId, + principal_id: assessmentSubmissionsRow.principal_id, + assessment_submission_state: + assessmentSubmissionsRow.assessment_submission_state, + opened_at: DateTime.fromSQL(assessmentSubmissionsRow.opened_at, { + zone: 'utc', + }).toISO(), + last_modified: DateTime.fromSQL(assessmentSubmissionsRow.updated_at, { + zone: 'utc', + }).toISO(), + }; + + if (assessmentSubmissionsRow.score !== null) { + assessmentSubmission.score = assessmentSubmissionsRow.score; + } + + if (assessmentSubmissionsRow.submitted_at !== null) { + assessmentSubmission.submitted_at = DateTime.fromSQL( + assessmentSubmissionsRow.submitted_at, + { zone: 'utc' } + ).toISO(); + } + + assessmentSubmissions.push(assessmentSubmission); + } + + return assessmentSubmissions; +}; + +/** + * Lists all submissions by a program participant for a given program + * assessment, if any. Does not include responses for those submissions. + * + * @param {number} participantPrincipalId - The row ID of the principals table + * that corresponds with a given program participant. + * @param {number} programAssessmentId - The row ID of the program_assessments + * table for a given program assessment. + * @returns {Promise} An array of AssessmentSubmission + * objects constructed from matching program assessment submissions, if any, + * not including their responses. + */ +export const listParticipantProgramAssessmentSubmissions = async ( + participantPrincipalId: number, + programAssessmentId: number +): Promise => { + const matchingAssessmentSubmissionsRows = await db('assessment_submissions') + .join( + 'assessment_submission_states', + 'assessment_submissions.assessment_submission_state_id', + 'assessment_submission_states.id' + ) + .select( + 'assessment_submissions.id as id', + 'assessment_submission_states.title as assessment_submission_state', + 'assessment_submissions.score', + 'assessment_submissions.opened_at', + 'assessment_submissions.submitted_at', + 'assessment_submissions.updated_at' + ) + .where('assessment_submissions.principal_id', participantPrincipalId) + .andWhere('assessment_submissions.assessment_id', programAssessmentId); + + if (matchingAssessmentSubmissionsRows.length === 0) { + return null; + } + + const assessmentSubmissions: AssessmentSubmission[] = []; + + for (const assessmentSubmissionsRow of matchingAssessmentSubmissionsRows) { + const assessmentSubmission: AssessmentSubmission = { + id: assessmentSubmissionsRow.id, + assessment_id: programAssessmentId, + principal_id: participantPrincipalId, + assessment_submission_state: + assessmentSubmissionsRow.assessment_submission_state, + opened_at: DateTime.fromSQL(assessmentSubmissionsRow.opened_at, { + zone: 'utc', + }).toISO(), + last_modified: DateTime.fromSQL(assessmentSubmissionsRow.updated_at, { + zone: 'utc', + }).toISO(), + }; + + if (assessmentSubmissionsRow.score !== null) { + assessmentSubmission.score = assessmentSubmissionsRow.score; + } + + if (assessmentSubmissionsRow.submitted_at !== null) { + assessmentSubmission.submitted_at = DateTime.fromSQL( + assessmentSubmissionsRow.submitted_at, + { zone: 'utc' } + ).toISO(); + } + + assessmentSubmissions.push(assessmentSubmission); + } + + return assessmentSubmissions; +}; + +/** + * Lists all row IDs of programs for which a principal is either enrolled as a + * participant or is designated as facilitator. + * + * @param {number} principalId - The row ID of the principals table that + * corresponds with a given program member. + * @returns {Promise} An array of row IDs for all matching programs + * for which the user is enrolled or is facilitating. + */ +export const listPrincipalEnrolledProgramIds = async ( + principalId: number +): Promise => { + const enrolledProgramsList = await db('program_participants') + .select('program_id') + .where({ principal_id: principalId }); + + if (enrolledProgramsList.length === 0) { + return null; + } + + const programList: number[] = enrolledProgramsList.map( + enrolledProgram => enrolledProgram.program_id + ); + + return programList; +}; + +/** + * Lists all available program assessments for a given program. + * + * @param {number} programId - The row ID of the programs table for a given + * program. + * @returns {Promise} An array of the ProgramAssessment + * objects constructed from matching program assessments, if any. + */ +export const listProgramAssessments = async ( + programId: number +): Promise => { + const matchingProgramAssessmentsRows = await db('program_assessments') + .select('id', 'assessment_id', 'available_after', 'due_date') + .where('program_id', programId); + + if (matchingProgramAssessmentsRows.length === 0) { + return null; + } + + const programAssessments: ProgramAssessment[] = []; + + for (const programAssessmentsRow of matchingProgramAssessmentsRows) { + const program = await findProgram(programId); + + programAssessments.push({ + id: programAssessmentsRow.id, + program_id: programId, + assessment_id: programAssessmentsRow.assessment_id, + available_after: DateTime.fromSQL(programAssessmentsRow.available_after, { + zone: program.time_zone, + }).toISO(), + due_date: DateTime.fromSQL(programAssessmentsRow.due_date, { + zone: program.time_zone, + }).toISO(), + }); + } + + return programAssessments; +}; + +/** + * Retrieves all program IDs that a given user is a facilitator for, matching a + * given curriculum ID. This is used for routes where we have a curriculum + * assessment ID and do not have a way to check if a user is allowed to make + * edits to that curriculum assessment. + * + * @param {number} principalId - The row ID of the principals table for the + * logged-in user. + * @param {number} curriculumId - The row ID of the curriculums table + * corresponding to the curriculum assessment we will be retrieving, + * modifying, or deleting. + * @returns {Promise} An array of the row IDs of the programs table + * matching the given curriculum ID. + */ +export const facilitatorProgramIdsMatchingCurriculum = async ( + principalId: number, + curriculumId: number +): Promise => { + const participatingProgramIds = await listPrincipalEnrolledProgramIds( + principalId + ); + + if (participatingProgramIds === null) { + return []; + } + + const curriculumPrograms = await listProgramsForCurriculum(curriculumId); + + const matchingFacilitatorPrograms: number[] = []; + + for (const programId of participatingProgramIds) { + const programRole = await getPrincipalProgramRole(principalId, programId); + + if (programRole === 'Facilitator') { + if ( + curriculumPrograms.filter(program => program.id === programId) + .length !== 0 + ) { + matchingFacilitatorPrograms.push(programId); + } + } + } + + return matchingFacilitatorPrograms; +}; + +/** + * Removes any possible grading information from an assessment submission in + * cases when we don't want to return that information to the requester. + * + * @param {AssessmentSubmission} assessmentSubmissionWithGrades - An + * AssessmentSubmission possibly containing grading information. + * @returns {AssessmentSubmission} The updated object with any possible grading + * information removed, without harming the original object. + */ +export const removeGradingInformation = ( + assessmentSubmissionWithGrades: AssessmentSubmission +): AssessmentSubmission => { + const gradeRemovedSubmission = { ...assessmentSubmissionWithGrades }; + delete gradeRemovedSubmission.score; + + if (assessmentSubmissionWithGrades.responses) { + const gradeRemovedResponses = assessmentSubmissionWithGrades.responses.map( + response => { + const gradeRemovedResponse = structuredClone(response); + delete gradeRemovedResponse.score; + delete gradeRemovedResponse.grader_response; + return gradeRemovedResponse; + } + ); + gradeRemovedSubmission.responses = gradeRemovedResponses; + } + + return gradeRemovedSubmission; +}; + +/** + * Updates a program assessment submission for a program participant, if their + * time has not expired, or updates a program assessment submission by the + * facilitator, if optional parameter passed is true. If the submission is + * expired and the function is not passed true for facilitatorOverride, the only + * update allowed will be to mark a program assessment submission "Expired" + * instead of "Opened" or "In Progress". + * + * @param {AssessmentSubmission} assessmentSubmission - The updated program + * assessment submission information, including responses. + * @param {boolean} [facilitatorOverride] - Optional specifier for when the + * program facilitator is the one updating a program assessment submission, + * skipping the submission expiration check. + * @returns {Promise} An AssessmentSubmission object + * constructed from the updated row in the assessment_submissions table. + */ +export const updateAssessmentSubmission = async ( + assessmentSubmission: AssessmentSubmission, + facilitatorOverride?: boolean +): Promise => { + const existingAssessmentSubmission = await getAssessmentSubmission( + assessmentSubmission.id, + true, + facilitatorOverride || false + ); + + const updatedSubmission = structuredClone(assessmentSubmission); + + let newState; + + if (facilitatorOverride && facilitatorOverride === true) { + const updatedResponses: AssessmentResponse[] = []; + + for (const assessmentResponse of assessmentSubmission.responses) { + updatedResponses.push( + await updateSubmissionResponse(assessmentResponse, true) + ); + } + + newState = assessmentSubmission.assessment_submission_state; + const [gradedStateId] = await db('assessment_submission_states') + .select('id') + .where('title', newState); + await db('assessment_submissions') + .update({ + assessment_submission_state_id: gradedStateId.id, + score: assessmentSubmission.score, + }) + .where('id', assessmentSubmission.id); + + updatedSubmission.responses = updatedResponses; + updatedSubmission.assessment_submission_state = newState; + } else if ( + ['Expired', 'Submitted', 'Graded'].includes( + existingAssessmentSubmission.assessment_submission_state + ) + ) { + return existingAssessmentSubmission; + } else if ( + ['Opened', 'In Progress'].includes( + existingAssessmentSubmission.assessment_submission_state + ) + ) { + const assessmentSubmissionNoGrades = + removeGradingInformation(assessmentSubmission); + + if ( + assessmentSubmission.assessment_submission_state === 'Expired' || + (await assessmentSubmissionExpired(assessmentSubmission)) + ) { + newState = 'Expired'; + + updatedSubmission.last_modified = DateTime.now() + .plus({ weeks: 1 }) + .toUTC() + .toISO(); + } else { + newState = + assessmentSubmission.assessment_submission_state === 'Submitted' + ? 'Submitted' + : 'In Progress'; + + const updatedResponses: AssessmentResponse[] = []; + + if ( + assessmentSubmission.responses && + Array.isArray(assessmentSubmission.responses) && + assessmentSubmission.responses.length > 0 + ) { + for (const assessmentResponse of assessmentSubmissionNoGrades.responses) { + const matchingExistingResponses = + existingAssessmentSubmission.responses?.filter( + e => e.id === assessmentResponse.id + ); + + if ( + !Array.isArray(existingAssessmentSubmission.responses) || + matchingExistingResponses.length === 0 + ) { + updatedResponses.push( + await createSubmissionResponse(assessmentResponse) + ); + } else { + const [existingResponse] = matchingExistingResponses; + + if ( + (typeof assessmentResponse.answer_id !== 'undefined' && + assessmentResponse.answer_id !== null && + existingResponse.answer_id !== assessmentResponse.answer_id) || + (typeof assessmentResponse.response_text !== 'undefined' && + assessmentResponse.response_text !== null && + existingResponse.response_text !== + assessmentResponse.response_text) + ) { + updatedResponses.push( + await updateSubmissionResponse(assessmentResponse, false) + ); + } else { + updatedResponses.push(assessmentResponse); + } + } + } + } + + updatedSubmission.responses = updatedResponses; + } + + const [newStateId] = await db('assessment_submission_states') + .select('id') + .where('title', newState); + + if (newState === 'Submitted') { + await db('assessment_submissions') + .update({ + assessment_submission_state_id: newStateId.id, + submitted_at: db.fn.now(), + }) + .where('id', assessmentSubmission.id); + } else { + await db('assessment_submissions') + .update({ assessment_submission_state_id: newStateId.id }) + .where('id', assessmentSubmission.id); + } + + updatedSubmission.assessment_submission_state = newState; + } + + return updatedSubmission; +}; + +/** + * Updates an existing curriculum assessment, its metadata, and its associated + * questions and answers if given. + * + * @param {CurriculumAssessment} curriculumAssessment - The updated curriculum + * assessment information with which to update the corresponding database + * data. + * @returns {Promise} The updated CurriculumAssessment + * object that was handed to us, if update was successful. + */ +export const updateCurriculumAssessment = async ( + curriculumAssessment: CurriculumAssessment +): Promise => { + const existingQuestions = await listAssessmentQuestions( + curriculumAssessment.id, + false, + false + ); + + const newQuestions: Question[] = []; + const updatedQuestions: Question[] = []; + + if ( + curriculumAssessment.questions && + Array.isArray(curriculumAssessment.questions) && + curriculumAssessment.questions.length > 0 + ) { + for (const question of curriculumAssessment.questions) { + if (question.id && typeof question.id === 'number' && question.id > 0) { + if ( + existingQuestions && + Array.isArray(existingQuestions) && + existingQuestions.length > 0 + ) { + const eqIndex = existingQuestions.findIndex( + existingQuestion => existingQuestion.id === question.id + ); + existingQuestions.splice(eqIndex, 1); + } + updatedQuestions.push(question); + } else { + newQuestions.push(question); + } + } + } + + if ( + existingQuestions && + Array.isArray(existingQuestions) && + existingQuestions.length > 0 + ) { + for (const deletedQuestion of existingQuestions) { + await deleteAssessmentQuestion(deletedQuestion.id); + } + } + + const newQuestionList: Question[] = []; + + for (const updatedQuestion of updatedQuestions) { + newQuestionList.push(await updateAssessmentQuestion(updatedQuestion)); + } + + for (const newQuestion of newQuestions) { + newQuestionList.push( + await createAssessmentQuestion(curriculumAssessment.id, newQuestion) + ); + } + + await db('curriculum_assessments') + .update({ + title: curriculumAssessment.title, + description: curriculumAssessment.description, + max_score: curriculumAssessment.max_score, + max_num_submissions: curriculumAssessment.max_num_submissions, + time_limit: curriculumAssessment.time_limit, + activity_id: curriculumAssessment.activity_id, + }) + .where('id', curriculumAssessment.id); + + curriculumAssessment.questions = newQuestionList; + + return curriculumAssessment; +}; + +/** + * Updates an existing program assessment in the program_assessments table. + * + * @param {ProgramAssessment} programAssessment - The updated program assessment + * information with which to update the corresponding database data. + * @returns {Promise} The updated ProgramAssessment object + * that was handed to us, if update was successful. + */ +export const updateProgramAssessment = async ( + programAssessment: ProgramAssessment +): Promise => { + await db('program_assessments') + .update({ + available_after: programAssessment.available_after, + due_date: programAssessment.due_date, + }) + .where('id', programAssessment.id); + + return programAssessment; +}; diff --git a/api/src/services/db.ts b/api/src/services/db.ts index 1ea08373..a6b10fca 100644 --- a/api/src/services/db.ts +++ b/api/src/services/db.ts @@ -10,7 +10,7 @@ const tzOffset = new IANAZone( ).formatOffset(DateTime.now().toMillis(), 'short'); const db = knex({ - client: 'mysql', + client: 'mysql2', connection: { database: findConfig('MYSQL_DATABASE', ''), host: findConfig('MYSQL_HOST', 'localhost'), diff --git a/api/src/services/questionnairesService.ts b/api/src/services/questionnairesService.ts index 98e4cc23..29ab1a43 100644 --- a/api/src/services/questionnairesService.ts +++ b/api/src/services/questionnairesService.ts @@ -1,13 +1,13 @@ import db from './db'; export const findQuestionnaire = async (questionnaireId: number) => { - const [questionnaire] = await db('questionnaires') + const [questionnaire] = await db('surveys') .select('id') .where({ id: questionnaireId }); if (!questionnaire) { return null; } - const prompts = await db('prompts') + const prompts = await db('survey_questions') .select('id', 'label', 'query_text') .where({ questionnaire_id: questionnaireId, @@ -17,7 +17,7 @@ export const findQuestionnaire = async (questionnaireId: number) => { const promptsById = new Map(); let options; if (promptIds.length) { - options = await db('options') + options = await db('survey_answers') .select('id', 'label', 'prompt_id') .whereIn('prompt_id', promptIds) .orderBy('prompt_id', 'sort_order'); diff --git a/api/src/services/reflectionsService.ts b/api/src/services/reflectionsService.ts index 806e0541..81c0bd9b 100644 --- a/api/src/services/reflectionsService.ts +++ b/api/src/services/reflectionsService.ts @@ -33,14 +33,18 @@ export const listReflections = async ( id: 'responses.id', reflection_id: 'reflection_id', option_id: 'option_id', - option_label: 'options.label', + option_label: 'survey_answers.label', prompt_id: 'prompt_id', - prompt_label: 'prompts.label', + prompt_label: 'survey_questions.label', }) - .join('options', 'responses.option_id', 'options.id') - .join('prompts', 'options.prompt_id', 'prompts.id') + .join('survey_answers', 'responses.option_id', 'survey_answers.id') + .join( + 'survey_questions', + 'survey_answers.prompt_id', + 'survey_questions.id' + ) .whereIn('reflection_id', reflectionIds) - .orderBy('prompts.sort_order'), + .orderBy('survey_questions.sort_order'), ]); const reflectionsById = new Map(); for (const reflection of reflections) { diff --git a/api/yarn.lock b/api/yarn.lock index 1326bed9..7180309c 100644 --- a/api/yarn.lock +++ b/api/yarn.lock @@ -2,79 +2,80 @@ # yarn lockfile v1 -"@ampproject/remapping@^2.1.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" - integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== dependencies: - "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" - integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.21.4": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.21.4.tgz#d0fa9e4413aca81f2b23b9442797bda1826edb39" + integrity sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g== dependencies: "@babel/highlight" "^7.18.6" -"@babel/compat-data@^7.20.5": - version "7.20.14" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.14.tgz#4106fc8b755f3e3ee0a0a7c27dde5de1d2b2baf8" - integrity sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw== +"@babel/compat-data@^7.21.5": + version "7.21.9" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.9.tgz#10a2e7fda4e51742c907938ac3b7229426515514" + integrity sha512-FUGed8kfhyWvbYug/Un/VPJD41rDIgoVVcR+FuzhzOYyRz5uED+Gd3SLZml0Uw2l2aHFb7ZgdW5mGA3G2cCCnQ== "@babel/core@^7.11.6", "@babel/core@^7.12.3": - version "7.20.12" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.12.tgz#7930db57443c6714ad216953d1356dac0eb8496d" - integrity sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg== - dependencies: - "@ampproject/remapping" "^2.1.0" - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.7" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helpers" "^7.20.7" - "@babel/parser" "^7.20.7" + version "7.21.8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.8.tgz#2a8c7f0f53d60100ba4c32470ba0281c92aa9aa4" + integrity sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.21.4" + "@babel/generator" "^7.21.5" + "@babel/helper-compilation-targets" "^7.21.5" + "@babel/helper-module-transforms" "^7.21.5" + "@babel/helpers" "^7.21.5" + "@babel/parser" "^7.21.8" "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.12" - "@babel/types" "^7.20.7" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" json5 "^2.2.2" semver "^6.3.0" -"@babel/generator@^7.20.7", "@babel/generator@^7.7.2": - version "7.20.14" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.14.tgz#9fa772c9f86a46c6ac9b321039400712b96f64ce" - integrity sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg== +"@babel/generator@^7.21.5", "@babel/generator@^7.7.2": + version "7.21.9" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.9.tgz#3a1b706e07d836e204aee0650e8ee878d3aaa241" + integrity sha512-F3fZga2uv09wFdEjEQIJxXALXfz0+JaOb7SabvVMmjHxeVTuGW8wgE8Vp1Hd7O+zMTYtcfEISGRzPkeiaPPsvg== dependencies: - "@babel/types" "^7.20.7" + "@babel/types" "^7.21.5" "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" -"@babel/helper-compilation-targets@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz#a6cd33e93629f5eb473b021aac05df62c4cd09bb" - integrity sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ== +"@babel/helper-compilation-targets@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz#631e6cc784c7b660417421349aac304c94115366" + integrity sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w== dependencies: - "@babel/compat-data" "^7.20.5" - "@babel/helper-validator-option" "^7.18.6" + "@babel/compat-data" "^7.21.5" + "@babel/helper-validator-option" "^7.21.0" browserslist "^4.21.3" lru-cache "^5.1.1" semver "^6.3.0" -"@babel/helper-environment-visitor@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" - integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== +"@babel/helper-environment-visitor@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz#c769afefd41d171836f7cb63e295bedf689d48ba" + integrity sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ== -"@babel/helper-function-name@^7.19.0": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" - integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== +"@babel/helper-function-name@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz#d552829b10ea9f120969304023cd0645fa00b1b4" + integrity sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg== dependencies: - "@babel/template" "^7.18.10" - "@babel/types" "^7.19.0" + "@babel/template" "^7.20.7" + "@babel/types" "^7.21.0" "@babel/helper-hoist-variables@^7.18.6": version "7.18.6" @@ -83,38 +84,38 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-module-imports@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" - integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== +"@babel/helper-module-imports@^7.21.4": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz#ac88b2f76093637489e718a90cec6cf8a9b029af" + integrity sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg== dependencies: - "@babel/types" "^7.18.6" + "@babel/types" "^7.21.4" -"@babel/helper-module-transforms@^7.20.11": - version "7.20.11" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz#df4c7af713c557938c50ea3ad0117a7944b2f1b0" - integrity sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg== +"@babel/helper-module-transforms@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz#d937c82e9af68d31ab49039136a222b17ac0b420" + integrity sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw== dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-environment-visitor" "^7.21.5" + "@babel/helper-module-imports" "^7.21.4" + "@babel/helper-simple-access" "^7.21.5" "@babel/helper-split-export-declaration" "^7.18.6" "@babel/helper-validator-identifier" "^7.19.1" "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.10" - "@babel/types" "^7.20.7" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.8.0": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" - integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz#345f2377d05a720a4e5ecfa39cbf4474a4daed56" + integrity sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg== -"@babel/helper-simple-access@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" - integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== +"@babel/helper-simple-access@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz#d697a7971a5c39eac32c7e63c0921c06c8a249ee" + integrity sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg== dependencies: - "@babel/types" "^7.20.2" + "@babel/types" "^7.21.5" "@babel/helper-split-export-declaration@^7.18.6": version "7.18.6" @@ -123,29 +124,29 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-string-parser@^7.19.4": - version "7.19.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" - integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== +"@babel/helper-string-parser@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz#2b3eea65443c6bdc31c22d037c65f6d323b6b2bd" + integrity sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w== "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== -"@babel/helper-validator-option@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" - integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== +"@babel/helper-validator-option@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180" + integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ== -"@babel/helpers@^7.20.7": - version "7.20.13" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.13.tgz#e3cb731fb70dc5337134cadc24cbbad31cc87ad2" - integrity sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg== +"@babel/helpers@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.5.tgz#5bac66e084d7a4d2d9696bdf0175a93f7fb63c08" + integrity sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA== dependencies: "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.13" - "@babel/types" "^7.20.7" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" "@babel/highlight@^7.18.6": version "7.18.6" @@ -156,10 +157,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.13", "@babel/parser@^7.20.7": - version "7.20.15" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.15.tgz#eec9f36d8eaf0948bb88c87a46784b5ee9fd0c89" - integrity sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.5", "@babel/parser@^7.21.8", "@babel/parser@^7.21.9": + version "7.21.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.9.tgz#ab18ea3b85b4bc33ba98a8d4c2032c557d23cf14" + integrity sha512-q5PNg/Bi1OpGgx5jYlvWZwAorZepEudDMCLtj967aeS7WMont7dUZI46M2XwcIQqvUlMxWfdLFu4S/qSxeUu5g== "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -197,11 +198,11 @@ "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-jsx@^7.7.2": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" - integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz#f264ed7bf40ffc9ec239edabc17a50c4f5b6fea2" + integrity sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" @@ -253,43 +254,43 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-typescript@^7.7.2": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz#4e9a0cfc769c85689b77a2e642d24e9f697fc8c7" - integrity sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.19.0" - -"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" - integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - -"@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.13", "@babel/traverse@^7.7.2": - version "7.20.13" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.13.tgz#817c1ba13d11accca89478bd5481b2d168d07473" - integrity sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.7" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.21.4.tgz#2751948e9b7c6d771a8efa59340c15d4a2891ff8" + integrity sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/template@^7.20.7", "@babel/template@^7.3.3": + version "7.21.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.21.9.tgz#bf8dad2859130ae46088a99c1f265394877446fb" + integrity sha512-MK0X5k8NKOuWRamiEfc3KEJiHMTkGZNUjzMipqCGDDc6ijRl/B7RGSKVGncu4Ro/HdyzzY6cmoXuKI2Gffk7vQ== + dependencies: + "@babel/code-frame" "^7.21.4" + "@babel/parser" "^7.21.9" + "@babel/types" "^7.21.5" + +"@babel/traverse@^7.21.5", "@babel/traverse@^7.7.2": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.5.tgz#ad22361d352a5154b498299d523cf72998a4b133" + integrity sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw== + dependencies: + "@babel/code-frame" "^7.21.4" + "@babel/generator" "^7.21.5" + "@babel/helper-environment-visitor" "^7.21.5" + "@babel/helper-function-name" "^7.21.0" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.13" - "@babel/types" "^7.20.7" + "@babel/parser" "^7.21.5" + "@babel/types" "^7.21.5" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.19.0", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.3.0", "@babel/types@^7.3.3": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f" - integrity sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg== +"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.5.tgz#18dfbd47c39d3904d5db3d3dc2cc80bedb60e5b6" + integrity sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q== dependencies: - "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-string-parser" "^7.21.5" "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" @@ -305,14 +306,26 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@eslint/eslintrc@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" - integrity sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA== +"@eslint-community/eslint-utils@^4.2.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.4.0": + version "4.5.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" + integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== + +"@eslint/eslintrc@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.3.tgz#4910db5505f4d503f27774bf356e3704818a0331" + integrity sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.4.0" + espree "^9.5.2" globals "^13.19.0" ignore "^5.2.0" import-fresh "^3.2.1" @@ -320,6 +333,11 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@eslint/js@8.41.0": + version "8.41.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.41.0.tgz#080321c3b68253522f7646b55b577dd99d2950b3" + integrity sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA== + "@humanwhocodes/config-array@^0.11.8": version "0.11.8" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" @@ -355,109 +373,109 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.4.2.tgz#f78374905c2454764152904a344a2d5226b0ef09" - integrity sha512-0I/rEJwMpV9iwi9cDEnT71a5nNGK9lj8Z4+1pRAU2x/thVXCDnaTGrvxyK+cAqZTFVFCiR+hfVrP4l2m+dCmQg== +"@jest/console@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.5.0.tgz#593a6c5c0d3f75689835f1b3b4688c4f8544cb57" + integrity sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ== dependencies: - "@jest/types" "^29.4.2" + "@jest/types" "^29.5.0" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^29.4.2" - jest-util "^29.4.2" + jest-message-util "^29.5.0" + jest-util "^29.5.0" slash "^3.0.0" -"@jest/core@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.4.2.tgz#6e999b67bdc2df9d96ba9b142465bda71ee472c2" - integrity sha512-KGuoQah0P3vGNlaS/l9/wQENZGNKGoWb+OPxh3gz+YzG7/XExvYu34MzikRndQCdM2S0tzExN4+FL37i6gZmCQ== +"@jest/core@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.5.0.tgz#76674b96904484e8214614d17261cc491e5f1f03" + integrity sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ== dependencies: - "@jest/console" "^29.4.2" - "@jest/reporters" "^29.4.2" - "@jest/test-result" "^29.4.2" - "@jest/transform" "^29.4.2" - "@jest/types" "^29.4.2" + "@jest/console" "^29.5.0" + "@jest/reporters" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" ci-info "^3.2.0" exit "^0.1.2" graceful-fs "^4.2.9" - jest-changed-files "^29.4.2" - jest-config "^29.4.2" - jest-haste-map "^29.4.2" - jest-message-util "^29.4.2" - jest-regex-util "^29.4.2" - jest-resolve "^29.4.2" - jest-resolve-dependencies "^29.4.2" - jest-runner "^29.4.2" - jest-runtime "^29.4.2" - jest-snapshot "^29.4.2" - jest-util "^29.4.2" - jest-validate "^29.4.2" - jest-watcher "^29.4.2" + jest-changed-files "^29.5.0" + jest-config "^29.5.0" + jest-haste-map "^29.5.0" + jest-message-util "^29.5.0" + jest-regex-util "^29.4.3" + jest-resolve "^29.5.0" + jest-resolve-dependencies "^29.5.0" + jest-runner "^29.5.0" + jest-runtime "^29.5.0" + jest-snapshot "^29.5.0" + jest-util "^29.5.0" + jest-validate "^29.5.0" + jest-watcher "^29.5.0" micromatch "^4.0.4" - pretty-format "^29.4.2" + pretty-format "^29.5.0" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.4.2.tgz#ee92c316ee2fbdf0bcd9d2db0ef42d64fea26b56" - integrity sha512-JKs3VUtse0vQfCaFGJRX1bir9yBdtasxziSyu+pIiEllAQOe4oQhdCYIf3+Lx+nGglFktSKToBnRJfD5QKp+NQ== +"@jest/environment@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.5.0.tgz#9152d56317c1fdb1af389c46640ba74ef0bb4c65" + integrity sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ== dependencies: - "@jest/fake-timers" "^29.4.2" - "@jest/types" "^29.4.2" + "@jest/fake-timers" "^29.5.0" + "@jest/types" "^29.5.0" "@types/node" "*" - jest-mock "^29.4.2" + jest-mock "^29.5.0" -"@jest/expect-utils@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.4.2.tgz#cd0065dfdd8e8a182aa350cc121db97b5eed7b3f" - integrity sha512-Dd3ilDJpBnqa0GiPN7QrudVs0cczMMHtehSo2CSTjm3zdHx0RcpmhFNVEltuEFeqfLIyWKFI224FsMSQ/nsJQA== +"@jest/expect-utils@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.5.0.tgz#f74fad6b6e20f924582dc8ecbf2cb800fe43a036" + integrity sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg== dependencies: - jest-get-type "^29.4.2" + jest-get-type "^29.4.3" -"@jest/expect@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.4.2.tgz#2d4a6a41b29380957c5094de19259f87f194578b" - integrity sha512-NUAeZVApzyaeLjfWIV/64zXjA2SS+NuUPHpAlO7IwVMGd5Vf9szTl9KEDlxY3B4liwLO31os88tYNHl6cpjtKQ== +"@jest/expect@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.5.0.tgz#80952f5316b23c483fbca4363ce822af79c38fba" + integrity sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g== dependencies: - expect "^29.4.2" - jest-snapshot "^29.4.2" + expect "^29.5.0" + jest-snapshot "^29.5.0" -"@jest/fake-timers@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.4.2.tgz#af43ee1a5720b987d0348f80df98f2cb17d45cd0" - integrity sha512-Ny1u0Wg6kCsHFWq7A/rW/tMhIedq2siiyHyLpHCmIhP7WmcAmd2cx95P+0xtTZlj5ZbJxIRQi4OPydZZUoiSQQ== +"@jest/fake-timers@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.5.0.tgz#d4d09ec3286b3d90c60bdcd66ed28d35f1b4dc2c" + integrity sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg== dependencies: - "@jest/types" "^29.4.2" + "@jest/types" "^29.5.0" "@sinonjs/fake-timers" "^10.0.2" "@types/node" "*" - jest-message-util "^29.4.2" - jest-mock "^29.4.2" - jest-util "^29.4.2" + jest-message-util "^29.5.0" + jest-mock "^29.5.0" + jest-util "^29.5.0" -"@jest/globals@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.4.2.tgz#73f85f5db0e17642258b25fd0b9fc89ddedb50eb" - integrity sha512-zCk70YGPzKnz/I9BNFDPlK+EuJLk21ur/NozVh6JVM86/YYZtZHqxFFQ62O9MWq7uf3vIZnvNA0BzzrtxD9iyg== +"@jest/globals@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.5.0.tgz#6166c0bfc374c58268677539d0c181f9c1833298" + integrity sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ== dependencies: - "@jest/environment" "^29.4.2" - "@jest/expect" "^29.4.2" - "@jest/types" "^29.4.2" - jest-mock "^29.4.2" + "@jest/environment" "^29.5.0" + "@jest/expect" "^29.5.0" + "@jest/types" "^29.5.0" + jest-mock "^29.5.0" -"@jest/reporters@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.4.2.tgz#6abfa923941daae0acc76a18830ee9e79a22042d" - integrity sha512-10yw6YQe75zCgYcXgEND9kw3UZZH5tJeLzWv4vTk/2mrS1aY50A37F+XT2hPO5OqQFFnUWizXD8k1BMiATNfUw== +"@jest/reporters@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.5.0.tgz#985dfd91290cd78ddae4914ba7921bcbabe8ac9b" + integrity sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^29.4.2" - "@jest/test-result" "^29.4.2" - "@jest/transform" "^29.4.2" - "@jest/types" "^29.4.2" + "@jest/console" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" "@jridgewell/trace-mapping" "^0.3.15" "@types/node" "*" chalk "^4.0.0" @@ -470,115 +488,117 @@ istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.1.3" - jest-message-util "^29.4.2" - jest-util "^29.4.2" - jest-worker "^29.4.2" + jest-message-util "^29.5.0" + jest-util "^29.5.0" + jest-worker "^29.5.0" slash "^3.0.0" string-length "^4.0.1" strip-ansi "^6.0.0" v8-to-istanbul "^9.0.1" -"@jest/schemas@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.2.tgz#cf7cfe97c5649f518452b176c47ed07486270fc1" - integrity sha512-ZrGzGfh31NtdVH8tn0mgJw4khQuNHiKqdzJAFbCaERbyCP9tHlxWuL/mnMu8P7e/+k4puWjI1NOzi/sFsjce/g== +"@jest/schemas@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.3.tgz#39cf1b8469afc40b6f5a2baaa146e332c4151788" + integrity sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg== dependencies: "@sinclair/typebox" "^0.25.16" -"@jest/source-map@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.4.2.tgz#f9815d59e25cd3d6828e41489cd239271018d153" - integrity sha512-tIoqV5ZNgYI9XCKXMqbYe5JbumcvyTgNN+V5QW4My033lanijvCD0D4PI9tBw4pRTqWOc00/7X3KVvUh+qnF4Q== +"@jest/source-map@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.4.3.tgz#ff8d05cbfff875d4a791ab679b4333df47951d20" + integrity sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w== dependencies: "@jridgewell/trace-mapping" "^0.3.15" callsites "^3.0.0" graceful-fs "^4.2.9" -"@jest/test-result@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.4.2.tgz#34b0ba069f2e3072261e4884c8fb6bd15ed6fb8d" - integrity sha512-HZsC3shhiHVvMtP+i55MGR5bPcc3obCFbA5bzIOb8pCjwBZf11cZliJncCgaVUbC5yoQNuGqCkC0Q3t6EItxZA== +"@jest/test-result@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.5.0.tgz#7c856a6ca84f45cc36926a4e9c6b57f1973f1408" + integrity sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ== dependencies: - "@jest/console" "^29.4.2" - "@jest/types" "^29.4.2" + "@jest/console" "^29.5.0" + "@jest/types" "^29.5.0" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.4.2.tgz#8b48e5bc4af80b42edacaf2a733d4f295edf28fb" - integrity sha512-9Z2cVsD6CcObIVrWigHp2McRJhvCxL27xHtrZFgNC1RwnoSpDx6fZo8QYjJmziFlW9/hr78/3sxF54S8B6v8rg== +"@jest/test-sequencer@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz#34d7d82d3081abd523dbddc038a3ddcb9f6d3cc4" + integrity sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ== dependencies: - "@jest/test-result" "^29.4.2" + "@jest/test-result" "^29.5.0" graceful-fs "^4.2.9" - jest-haste-map "^29.4.2" + jest-haste-map "^29.5.0" slash "^3.0.0" -"@jest/transform@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.4.2.tgz#b24b72dbab4c8675433a80e222d6a8ef4656fb81" - integrity sha512-kf1v5iTJHn7p9RbOsBuc/lcwyPtJaZJt5885C98omWz79NIeD3PfoiiaPSu7JyCyFzNOIzKhmMhQLUhlTL9BvQ== +"@jest/transform@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.5.0.tgz#cf9c872d0965f0cbd32f1458aa44a2b1988b00f9" + integrity sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw== dependencies: "@babel/core" "^7.11.6" - "@jest/types" "^29.4.2" + "@jest/types" "^29.5.0" "@jridgewell/trace-mapping" "^0.3.15" babel-plugin-istanbul "^6.1.1" chalk "^4.0.0" convert-source-map "^2.0.0" fast-json-stable-stringify "^2.1.0" graceful-fs "^4.2.9" - jest-haste-map "^29.4.2" - jest-regex-util "^29.4.2" - jest-util "^29.4.2" + jest-haste-map "^29.5.0" + jest-regex-util "^29.4.3" + jest-util "^29.5.0" micromatch "^4.0.4" pirates "^4.0.4" slash "^3.0.0" write-file-atomic "^4.0.2" -"@jest/types@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.4.2.tgz#8f724a414b1246b2bfd56ca5225d9e1f39540d82" - integrity sha512-CKlngyGP0fwlgC1BRUtPZSiWLBhyS9dKwKmyGxk8Z6M82LBEGB2aLQSg+U1MyLsU+M7UjnlLllBM2BLWKVm/Uw== +"@jest/types@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.5.0.tgz#f59ef9b031ced83047c67032700d8c807d6e1593" + integrity sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog== dependencies: - "@jest/schemas" "^29.4.2" + "@jest/schemas" "^29.4.3" "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" "@types/node" "*" "@types/yargs" "^17.0.8" chalk "^4.0.0" -"@jridgewell/gen-mapping@^0.1.0": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" - integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== - dependencies: - "@jridgewell/set-array" "^1.0.0" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jridgewell/gen-mapping@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" - integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== dependencies: "@jridgewell/set-array" "^1.0.1" "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3": +"@jridgewell/resolve-uri@3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== -"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== -"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": +"@jridgewell/sourcemap-codec@1.4.14": version "1.4.14" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" @@ -587,10 +607,10 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.17" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" - integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" + integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== dependencies: "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" @@ -616,24 +636,58 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@redis/bloom@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71" + integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg== + +"@redis/client@1.5.7": + version "1.5.7" + resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.7.tgz#92cc5c98c76f189e37d24f0e1e17e104c6af17d4" + integrity sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw== + dependencies: + cluster-key-slot "1.1.2" + generic-pool "3.9.0" + yallist "4.0.0" + +"@redis/graph@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.1.0.tgz#cc2b82e5141a29ada2cce7d267a6b74baa6dd519" + integrity sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg== + +"@redis/json@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.4.tgz#f372b5f93324e6ffb7f16aadcbcb4e5c3d39bda1" + integrity sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw== + +"@redis/search@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.1.2.tgz#6a8f66ba90812d39c2457420f859ce8fbd8f3838" + integrity sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA== + +"@redis/time-series@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.4.tgz#af85eb080f6934580e4d3b58046026b6c2b18717" + integrity sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng== + "@sinclair/typebox@^0.25.16": - version "0.25.21" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.21.tgz#763b05a4b472c93a8db29b2c3e359d55b29ce272" - integrity sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g== + version "0.25.24" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718" + integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ== -"@sinonjs/commons@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" - integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg== +"@sinonjs/commons@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72" + integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA== dependencies: type-detect "4.0.8" "@sinonjs/fake-timers@^10.0.2": - version "10.0.2" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c" - integrity sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw== + version "10.2.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.2.0.tgz#b3e322a34c5f26e3184e7f6115695f299c1b1194" + integrity sha512-OPwQlEdg40HAj5KNF8WW6q2KG4Z+cBCZb3m4ninfTZKaBmbIJodviQsDBoYMPHkOyJJMHnOJo5j2+LKDOhOACg== dependencies: - "@sinonjs/commons" "^2.0.0" + "@sinonjs/commons" "^3.0.0" "@socket.io/component-emitter@~3.1.0": version "3.1.0" @@ -656,9 +710,9 @@ integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== "@tsconfig/node16@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" - integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== "@types/babel__core@^7.1.14": version "7.20.0" @@ -687,9 +741,9 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.18.3" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.3.tgz#dfc508a85781e5698d5b33443416b6268c4b3e8d" - integrity sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w== + version "7.18.5" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.5.tgz#c107216842905afafd3b6e774f6f935da6f5db80" + integrity sha512-enCvTL8m/EHS/zIvJno9nE+ndYPh1/oNFzRYRmtUqJICG2VnCSBzMLW5VN2KCQU91f23tsNKR8v7VJJQMatl7Q== dependencies: "@babel/types" "^7.3.0" @@ -701,16 +755,6 @@ "@types/connect" "*" "@types/node" "*" -"@types/connect-redis@^0.0.19": - version "0.0.19" - resolved "https://registry.yarnpkg.com/@types/connect-redis/-/connect-redis-0.0.19.tgz#0895bbb1f2e3b5d2695158ec3f3ce367d1fca455" - integrity sha512-2312okmqA8MtogPkLmgwmM12VeSYH8gUAuRSzAtVz3PBoyEZwqt7Ri1lXBFtJmIVd3oXC/Hvg1KJSkt9x2ukKw== - dependencies: - "@types/express" "*" - "@types/express-session" "*" - "@types/ioredis" "^4.28.10" - "@types/redis" "^2.8.0" - "@types/connect@*": version "3.4.35" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" @@ -736,18 +780,19 @@ "@types/node" "*" "@types/express-serve-static-core@^4.17.33": - version "4.17.33" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543" - integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA== + version "4.17.35" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz#c95dd4424f0d32e525d23812aa8ab8e4d3906c4f" + integrity sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" + "@types/send" "*" -"@types/express-session@*", "@types/express-session@^1.17.4": - version "1.17.5" - resolved "https://registry.yarnpkg.com/@types/express-session/-/express-session-1.17.5.tgz#13f48852b4aa60ff595835faeb4b4dda0ba0866e" - integrity sha512-l0DhkvNVfyUPEEis8fcwbd46VptfA/jmMwHfob2TfDMf3HyPLiB9mKD71LXhz5TMUobODXPD27zXSwtFQLHm+w== +"@types/express-session@^1.17.7": + version "1.17.7" + resolved "https://registry.yarnpkg.com/@types/express-session/-/express-session-1.17.7.tgz#ced215c1244cb594be10e39f2781ddcd650be9a6" + integrity sha512-L25080PBYoRLu472HY/HNCxaXY8AaGgqGC8/p/8+BYMhG0RDOLQ1wpXOpAzr4Gi5TGozTKyJv5BVODM5UNyVMw== dependencies: "@types/express" "*" @@ -768,13 +813,6 @@ dependencies: "@types/node" "*" -"@types/ioredis@^4.28.10": - version "4.28.10" - resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.10.tgz#40ceb157a4141088d1394bb87c98ed09a75a06ff" - integrity sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ== - dependencies: - "@types/node" "*" - "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -794,10 +832,10 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^29.4.0": - version "29.4.0" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.4.0.tgz#a8444ad1704493e84dbf07bb05990b275b3b9206" - integrity sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ== +"@types/jest@^29.5.1": + version "29.5.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.1.tgz#83c818aa9a87da27d6da85d3378e5a34d2f31a47" + integrity sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ== dependencies: expect "^29.0.0" pretty-format "^29.0.0" @@ -807,20 +845,25 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== -"@types/luxon@^3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.2.0.tgz#99901b4ab29a5fdffc88fff59b3b47fbfbe0557b" - integrity sha512-lGmaGFoaXHuOLXFvuju2bfvZRqxAqkHPx9Y9IQdQABrinJJshJwfNCKV+u7rR3kJbiqfTF/NhOkcxxAFrObyaA== +"@types/luxon@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.3.0.tgz#a61043a62c0a72696c73a0a305c544c96501e006" + integrity sha512-uKRI5QORDnrGFYgcdAVnHvEIvEZ8noTpP/Bg+HeUzZghwinDlIS87DEenV5r1YoOF9G4x600YsUXLWZ19rmTmg== "@types/mime@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== -"@types/mock-knex@^0.4.3": - version "0.4.4" - resolved "https://registry.yarnpkg.com/@types/mock-knex/-/mock-knex-0.4.4.tgz#3631f9995eded35f763cc1b387ffc7e385f3f4a5" - integrity sha512-IbRF/6EKVZ9iHIu0e7xFR0dwBmJJ53hS6+mLp/EuXxRJ1828sxJ3pA7akl5ex4gC6x9BQ1R4QbXiSVurucHhbw== +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== + +"@types/mock-knex@^0.4.5": + version "0.4.5" + resolved "https://registry.yarnpkg.com/@types/mock-knex/-/mock-knex-0.4.5.tgz#33df948a75b3d70d26f1cb9baa6fd11f8e228d8d" + integrity sha512-vWDRgK86CnM7cISJI7eA7avW4VB/+s9Ux0AaAXIsYYkabbUgyLVxrnAyBWBWD6MAacZPDpCCd6VDf8L1ajbFdg== dependencies: "@types/node" "*" @@ -831,10 +874,10 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@>=10.0.0", "@types/node@^18.13.0": - version "18.13.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850" - integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg== +"@types/node@*", "@types/node@>=10.0.0", "@types/node@^20.2.3": + version "20.2.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.3.tgz#b31eb300610c3835ac008d690de6f87e28f9b878" + integrity sha512-pg9d0yC4rVNWQzX8U7xb4olIOFuuVL9za3bzMT2pu2SU0SNEi66i2qrvhE2qt0HvkhuCaWJu7pLNOt/Pj8BIrw== "@types/prettier@^2.1.5": version "2.7.2" @@ -851,22 +894,23 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== -"@types/redis@^2.8.0": - version "2.8.32" - resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.32.tgz#1d3430219afbee10f8cfa389dad2571a05ecfb11" - integrity sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w== +"@types/semver@^7.3.12": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" + integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== + +"@types/send@*": + version "0.17.1" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301" + integrity sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q== dependencies: + "@types/mime" "^1" "@types/node" "*" -"@types/semver@^7.3.12": - version "7.3.13" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" - integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== - "@types/serve-static@*": - version "1.15.0" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155" - integrity sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg== + version "1.15.1" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.1.tgz#86b1753f0be4f9a1bee68d459fcda5be4ea52b5d" + integrity sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ== dependencies: "@types/mime" "*" "@types/node" "*" @@ -876,10 +920,10 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== -"@types/superagent@*", "@types/superagent@^4.1.13": - version "4.1.16" - resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.16.tgz#12c9c16f232f9d89beab91d69368f96ce8e2d881" - integrity sha512-tLfnlJf6A5mB6ddqF159GqcDizfzbMUB1/DeT59/wBNqzRTNNKsaw79A/1TZ84X+f/EwWH8FeuSkjlCLyqS/zQ== +"@types/superagent@*", "@types/superagent@^4.1.17": + version "4.1.17" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.17.tgz#c8f0162b5d8a9c52d38b81398ef0650ef974b452" + integrity sha512-FFK/rRjNy24U6J1BvQkaNWu2ohOIF/kxRQXRsbT141YQODcOcZjzlcc4DGdI2SkTa0rhmF+X14zu6ICjCGIg+w== dependencies: "@types/cookiejar" "*" "@types/node" "*" @@ -897,94 +941,94 @@ integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== "@types/yargs@^17.0.8": - version "17.0.22" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.22.tgz#7dd37697691b5f17d020f3c63e7a45971ff71e9a" - integrity sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g== + version "17.0.24" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902" + integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw== dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.51.0": - version "5.51.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.51.0.tgz#da3f2819633061ced84bb82c53bba45a6fe9963a" - integrity sha512-wcAwhEWm1RgNd7dxD/o+nnLW8oH+6RK1OGnmbmkj/GGoDPV1WWMVP0FXYQBivKHdwM1pwii3bt//RC62EriIUQ== +"@typescript-eslint/eslint-plugin@^5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.7.tgz#e470af414f05ecfdc05a23e9ce6ec8f91db56fe2" + integrity sha512-BL+jYxUFIbuYwy+4fF86k5vdT9lT0CNJ6HtwrIvGh0PhH8s0yy5rjaKH2fDCrz5ITHy07WCzVGNvAmjJh4IJFA== dependencies: - "@typescript-eslint/scope-manager" "5.51.0" - "@typescript-eslint/type-utils" "5.51.0" - "@typescript-eslint/utils" "5.51.0" + "@eslint-community/regexpp" "^4.4.0" + "@typescript-eslint/scope-manager" "5.59.7" + "@typescript-eslint/type-utils" "5.59.7" + "@typescript-eslint/utils" "5.59.7" debug "^4.3.4" grapheme-splitter "^1.0.4" ignore "^5.2.0" natural-compare-lite "^1.4.0" - regexpp "^3.2.0" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.51.0": - version "5.51.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.51.0.tgz#2d74626652096d966ef107f44b9479f02f51f271" - integrity sha512-fEV0R9gGmfpDeRzJXn+fGQKcl0inIeYobmmUWijZh9zA7bxJ8clPhV9up2ZQzATxAiFAECqPQyMDB4o4B81AaA== +"@typescript-eslint/parser@^5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.7.tgz#02682554d7c1028b89aa44a48bf598db33048caa" + integrity sha512-VhpsIEuq/8i5SF+mPg9jSdIwgMBBp0z9XqjiEay+81PYLJuroN+ET1hM5IhkiYMJd9MkTz8iJLt7aaGAgzWUbQ== dependencies: - "@typescript-eslint/scope-manager" "5.51.0" - "@typescript-eslint/types" "5.51.0" - "@typescript-eslint/typescript-estree" "5.51.0" + "@typescript-eslint/scope-manager" "5.59.7" + "@typescript-eslint/types" "5.59.7" + "@typescript-eslint/typescript-estree" "5.59.7" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.51.0": - version "5.51.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.51.0.tgz#ad3e3c2ecf762d9a4196c0fbfe19b142ac498990" - integrity sha512-gNpxRdlx5qw3yaHA0SFuTjW4rxeYhpHxt491PEcKF8Z6zpq0kMhe0Tolxt0qjlojS+/wArSDlj/LtE69xUJphQ== +"@typescript-eslint/scope-manager@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.7.tgz#0243f41f9066f3339d2f06d7f72d6c16a16769e2" + integrity sha512-FL6hkYWK9zBGdxT2wWEd2W8ocXMu3K94i3gvMrjXpx+koFYdYV7KprKfirpgY34vTGzEPPuKoERpP8kD5h7vZQ== dependencies: - "@typescript-eslint/types" "5.51.0" - "@typescript-eslint/visitor-keys" "5.51.0" + "@typescript-eslint/types" "5.59.7" + "@typescript-eslint/visitor-keys" "5.59.7" -"@typescript-eslint/type-utils@5.51.0": - version "5.51.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.51.0.tgz#7af48005531700b62a20963501d47dfb27095988" - integrity sha512-QHC5KKyfV8sNSyHqfNa0UbTbJ6caB8uhcx2hYcWVvJAZYJRBo5HyyZfzMdRx8nvS+GyMg56fugMzzWnojREuQQ== +"@typescript-eslint/type-utils@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.7.tgz#89c97291371b59eb18a68039857c829776f1426d" + integrity sha512-ozuz/GILuYG7osdY5O5yg0QxXUAEoI4Go3Do5xeu+ERH9PorHBPSdvD3Tjp2NN2bNLh1NJQSsQu2TPu/Ly+HaQ== dependencies: - "@typescript-eslint/typescript-estree" "5.51.0" - "@typescript-eslint/utils" "5.51.0" + "@typescript-eslint/typescript-estree" "5.59.7" + "@typescript-eslint/utils" "5.59.7" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.51.0": - version "5.51.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.51.0.tgz#e7c1622f46c7eea7e12bbf1edfb496d4dec37c90" - integrity sha512-SqOn0ANn/v6hFn0kjvLwiDi4AzR++CBZz0NV5AnusT2/3y32jdc0G4woXPWHCumWtUXZKPAS27/9vziSsC9jnw== +"@typescript-eslint/types@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.7.tgz#6f4857203fceee91d0034ccc30512d2939000742" + integrity sha512-UnVS2MRRg6p7xOSATscWkKjlf/NDKuqo5TdbWck6rIRZbmKpVNTLALzNvcjIfHBE7736kZOFc/4Z3VcZwuOM/A== -"@typescript-eslint/typescript-estree@5.51.0": - version "5.51.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.51.0.tgz#0ec8170d7247a892c2b21845b06c11eb0718f8de" - integrity sha512-TSkNupHvNRkoH9FMA3w7TazVFcBPveAAmb7Sz+kArY6sLT86PA5Vx80cKlYmd8m3Ha2SwofM1KwraF24lM9FvA== +"@typescript-eslint/typescript-estree@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.7.tgz#b887acbd4b58e654829c94860dbff4ac55c5cff8" + integrity sha512-4A1NtZ1I3wMN2UGDkU9HMBL+TIQfbrh4uS0WDMMpf3xMRursDbqEf1ahh6vAAe3mObt8k3ZATnezwG4pdtWuUQ== dependencies: - "@typescript-eslint/types" "5.51.0" - "@typescript-eslint/visitor-keys" "5.51.0" + "@typescript-eslint/types" "5.59.7" + "@typescript-eslint/visitor-keys" "5.59.7" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.51.0": - version "5.51.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.51.0.tgz#074f4fabd5b12afe9c8aa6fdee881c050f8b4d47" - integrity sha512-76qs+5KWcaatmwtwsDJvBk4H76RJQBFe+Gext0EfJdC3Vd2kpY2Pf//OHHzHp84Ciw0/rYoGTDnIAr3uWhhJYw== +"@typescript-eslint/utils@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.7.tgz#7adf068b136deae54abd9a66ba5a8780d2d0f898" + integrity sha512-yCX9WpdQKaLufz5luG4aJbOpdXf/fjwGMcLFXZVPUz3QqLirG5QcwwnIHNf8cjLjxK4qtzTO8udUtMQSAToQnQ== dependencies: + "@eslint-community/eslint-utils" "^4.2.0" "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.51.0" - "@typescript-eslint/types" "5.51.0" - "@typescript-eslint/typescript-estree" "5.51.0" + "@typescript-eslint/scope-manager" "5.59.7" + "@typescript-eslint/types" "5.59.7" + "@typescript-eslint/typescript-estree" "5.59.7" eslint-scope "^5.1.1" - eslint-utils "^3.0.0" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.51.0": - version "5.51.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.51.0.tgz#c0147dd9a36c0de758aaebd5b48cae1ec59eba87" - integrity sha512-Oh2+eTdjHjOFjKA27sxESlA87YPSOJafGCR0md5oeMdh1ZcCfAGCIOL216uTBAkAIptvLIfKQhl7lHxMJet4GQ== +"@typescript-eslint/visitor-keys@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.7.tgz#09c36eaf268086b4fbb5eb9dc5199391b6485fc5" + integrity sha512-tyN+X2jvMslUszIiYbF0ZleP+RqQsFVpGrKI6e0Eet1w8WmhsAtmzaqm8oM8WJQ1ysLwhnsK/4hYHJjOgJVfQQ== dependencies: - "@typescript-eslint/types" "5.51.0" + "@typescript-eslint/types" "5.59.7" eslint-visitor-keys "^3.3.0" abbrev@1: @@ -1106,15 +1150,15 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -babel-jest@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.4.2.tgz#b17b9f64be288040877cbe2649f91ac3b63b2ba6" - integrity sha512-vcghSqhtowXPG84posYkkkzcZsdayFkubUgbE3/1tuGbX7AQtwCkkNA/wIbB0BMjuCPoqTkiDyKN7Ty7d3uwNQ== +babel-jest@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.5.0.tgz#3fe3ddb109198e78b1c88f9ebdecd5e4fc2f50a5" + integrity sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q== dependencies: - "@jest/transform" "^29.4.2" + "@jest/transform" "^29.5.0" "@types/babel__core" "^7.1.14" babel-plugin-istanbul "^6.1.1" - babel-preset-jest "^29.4.2" + babel-preset-jest "^29.5.0" chalk "^4.0.0" graceful-fs "^4.2.9" slash "^3.0.0" @@ -1130,10 +1174,10 @@ babel-plugin-istanbul@^6.1.1: istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.4.2.tgz#22aa43e255230f02371ffef1cac7eedef58f60bc" - integrity sha512-5HZRCfMeWypFEonRbEkwWXtNS1sQK159LhRVyRuLzyfVBxDy/34Tr/rg4YVi0SScSJ4fqeaR/OIeceJ/LaQ0pQ== +babel-plugin-jest-hoist@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz#a97db437936f441ec196990c9738d4b88538618a" + integrity sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" @@ -1158,12 +1202,12 @@ babel-preset-current-node-syntax@^1.0.0: "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-top-level-await" "^7.8.3" -babel-preset-jest@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.4.2.tgz#f0b20c6a79a9f155515e72a2d4f537fe002a4e38" - integrity sha512-ecWdaLY/8JyfUDr0oELBMpj3R5I1L6ZqG+kRJmwqfHtLWuPrJStR0LUkvUhfykJWTsXXMnohsayN/twltBbDrQ== +babel-preset-jest@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz#57bc8cc88097af7ff6a5ab59d1cd29d52a5916e2" + integrity sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg== dependencies: - babel-plugin-jest-hoist "^29.4.2" + babel-plugin-jest-hoist "^29.5.0" babel-preset-current-node-syntax "^1.0.0" balanced-match@^1.0.0: @@ -1176,11 +1220,6 @@ base64id@2.0.0, base64id@~2.0.0: resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== -bignumber.js@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075" - integrity sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A== - binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -1282,9 +1321,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001449: - version "1.0.30001451" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001451.tgz#2e197c698fc1373d63e1406d6607ea4617c613f1" - integrity sha512-XY7UbUpGRatZzoRft//5xOa69/1iGJRBlrieH6QYrkKLIFn3m7OVEJ81dSrKoy2BnKsdbX5cLrOispZNYo9v2w== + version "1.0.30001489" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001489.tgz#ca82ee2d4e4dbf2bd2589c9360d3fcc2c7ba3bd8" + integrity sha512-x1mgZEXK8jHIfAxm+xgdpHpk50IN3z3q3zP261/WS+uvePxW8izXuCu6AHz0lkuYTlATDehiZ/tNyYBdSQsOUQ== chalk@^2.0.0: version "2.4.2" @@ -1324,9 +1363,9 @@ chokidar@^3.5.2: fsevents "~2.3.2" ci-info@^3.2.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.1.tgz#708a6cdae38915d597afdf3b145f2f8e1ff55f3f" - integrity sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w== + version "3.8.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" + integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== cjs-module-lexer@^1.0.0: version "1.2.2" @@ -1342,6 +1381,11 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" +cluster-key-slot@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -1403,10 +1447,10 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -connect-redis@^6.0.0: - version "6.1.3" - resolved "https://registry.yarnpkg.com/connect-redis/-/connect-redis-6.1.3.tgz#0a83c953f9ece45ae37d304a8e8d1c3c6a60b4b9" - integrity sha512-aaNluLlAn/3JPxRwdzw7lhvEoU6Enb+d83xnokUNhC9dktqBoawKWL+WuxinxvBLTz6q9vReTnUDnUslaz74aw== +connect-redis@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/connect-redis/-/connect-redis-7.1.0.tgz#6618334697f2ed91536848cdc03734def2138acf" + integrity sha512-UaqO1EirWjON2ENsyau7N5lbkrdYBpS6mYlXSeff/OYXsd6EGZ+SXSmNPoljL2PSua8fgjAEaldSA73PMZQ9Eg== console-polyfill@0.3.0: version "0.3.0" @@ -1455,11 +1499,6 @@ cookiejar@^2.1.4: resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - cors@^2.8.5, cors@~2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" @@ -1521,19 +1560,19 @@ deep-is@^0.1.3: integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== deepmerge@^4.2.2: - version "4.3.0" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.0.tgz#65491893ec47756d44719ae520e0e2609233b59b" - integrity sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og== + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -denque@^1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" - integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== depd@2.0.0, depd@~2.0.0: version "2.0.0" @@ -1558,10 +1597,10 @@ dezalgo@^1.0.4: asap "^2.0.0" wrappy "1" -diff-sequences@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.2.tgz#711fe6bd8a5869fe2539cee4a5152425ff671fda" - integrity sha512-R6P0Y6PrsH3n4hUXxL3nns0rbRk6Q33js3ygJBeEpbzLzgcNuJ61+u0RXasFpTKISw99TxUzFnumSnRLsjhLaw== +diff-sequences@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" + integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== diff@^4.0.1: version "4.0.2" @@ -1593,9 +1632,9 @@ ee-first@1.1.1: integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== electron-to-chromium@^1.4.284: - version "1.4.294" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.294.tgz#ad80317b85f0859a9454680fbc1c726fefa7e6fd" - integrity sha512-PuHZB3jEN7D8WPPjLmBQAsqQz8tWHlkkB4n0E2OYw8RwVdmBYV0Wn+rUFH8JqYyIRb4HQhhedgxlZL163wqLrQ== + version "1.4.402" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.402.tgz#9aa7bbb63081513127870af6d22f829344c5ba57" + integrity sha512-gWYvJSkohOiBE6ecVYXkrDgNaUjo47QEKK0kQzmWyhkH+yoYiG44bwuicTGNSIQRG3WDMsWVZJLRnJnLNkbWvA== emittery@^0.13.1: version "0.13.1" @@ -1617,10 +1656,10 @@ engine.io-parser@~5.0.3: resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45" integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw== -engine.io@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.4.0.tgz#de27f79ecb58301171aea7956f3f6f4fa578490a" - integrity sha512-OgxY1c/RuCSeO/rTr8DIFXx76IzUUft86R7/P7MMbbkuzeqJoTNw2lmeD91IyGz41QYleIIjWeMJGgug043sfQ== +engine.io@~6.4.1: + version "6.4.2" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.4.2.tgz#ffeaf68f69b1364b0286badddf15ff633476473f" + integrity sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg== dependencies: "@types/cookie" "^0.4.1" "@types/cors" "^2.8.12" @@ -1672,10 +1711,10 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-prettier@^8.6.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz#dec1d29ab728f4fa63061774e1672ac4e363d207" - integrity sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA== +eslint-config-prettier@^8.8.0: + version "8.8.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348" + integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA== eslint-scope@^5.1.1: version "5.1.1" @@ -1685,37 +1724,28 @@ eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" - integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== +eslint-scope@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b" + integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - -eslint-visitor-keys@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" - integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" + integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== -eslint@^8.33.0: - version "8.33.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.33.0.tgz#02f110f32998cb598c6461f24f4d306e41ca33d7" - integrity sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA== +eslint@^8.41.0: + version "8.41.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.41.0.tgz#3062ca73363b4714b16dbc1e60f035e6134b6f1c" + integrity sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q== dependencies: - "@eslint/eslintrc" "^1.4.1" + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.4.0" + "@eslint/eslintrc" "^2.0.3" + "@eslint/js" "8.41.0" "@humanwhocodes/config-array" "^0.11.8" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -1725,24 +1755,22 @@ eslint@^8.33.0: debug "^4.3.2" doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.3.0" - espree "^9.4.0" - esquery "^1.4.0" + eslint-scope "^7.2.0" + eslint-visitor-keys "^3.4.1" + espree "^9.5.2" + esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" find-up "^5.0.0" glob-parent "^6.0.2" globals "^13.19.0" - grapheme-splitter "^1.0.4" + graphemer "^1.4.0" ignore "^5.2.0" import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" is-path-inside "^3.0.3" - js-sdsl "^4.1.4" js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" @@ -1750,7 +1778,6 @@ eslint@^8.33.0: minimatch "^3.1.2" natural-compare "^1.4.0" optionator "^0.9.1" - regexpp "^3.2.0" strip-ansi "^6.0.1" strip-json-comments "^3.1.0" text-table "^0.2.0" @@ -1760,24 +1787,24 @@ esm@^3.2.25: resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== -espree@^9.4.0: - version "9.4.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.1.tgz#51d6092615567a2c2cff7833445e37c28c0065bd" - integrity sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg== +espree@^9.5.2: + version "9.5.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.2.tgz#e994e7dc33a082a7a82dceaf12883a829353215b" + integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw== dependencies: acorn "^8.8.0" acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.3.0" + eslint-visitor-keys "^3.4.1" esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== dependencies: estraverse "^5.1.0" @@ -1828,16 +1855,16 @@ exit@^0.1.2: resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== -expect@^29.0.0, expect@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/expect/-/expect-29.4.2.tgz#2ae34eb88de797c64a1541ad0f1e2ea8a7a7b492" - integrity sha512-+JHYg9O3hd3RlICG90OPVjRkPBoiUH7PxvDVMnRiaq1g6JUgZStX514erMl0v2Dc5SkfVbm7ztqbd6qHHPn+mQ== +expect@^29.0.0, expect@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.5.0.tgz#68c0509156cb2a0adb8865d413b137eeaae682f7" + integrity sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg== dependencies: - "@jest/expect-utils" "^29.4.2" - jest-get-type "^29.4.2" - jest-matcher-utils "^29.4.2" - jest-message-util "^29.4.2" - jest-util "^29.4.2" + "@jest/expect-utils" "^29.5.0" + jest-get-type "^29.4.3" + jest-matcher-utils "^29.5.0" + jest-message-util "^29.5.0" + jest-util "^29.5.0" express-session@^1.17.2: version "1.17.3" @@ -2042,6 +2069,18 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +generate-function@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== + dependencies: + is-property "^1.0.2" + +generic-pool@3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4" + integrity sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -2053,12 +2092,13 @@ get-caller-file@^2.0.5: integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-intrinsic@^1.0.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" - integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== dependencies: function-bind "^1.1.1" has "^1.0.3" + has-proto "^1.0.1" has-symbols "^1.0.3" get-package-type@^0.1.0: @@ -2127,15 +2167,20 @@ globby@^11.1.0: slash "^3.0.0" graceful-fs@^4.2.9: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== grapheme-splitter@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -2146,6 +2191,11 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" @@ -2158,10 +2208,10 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" -helmet@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/helmet/-/helmet-6.0.1.tgz#52ec353638b2e87f14fe079d142b368ac11e79a4" - integrity sha512-8wo+VdQhTMVBMCITYZaGTbE4lvlthelPYSvoyNvk4RECTmrVjMerp9RfUOQXZWLvCcAn1pKj7ZRxK4lI9Alrcw== +helmet@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-7.0.0.tgz#ac3011ba82fa2467f58075afa58a49427ba6212d" + integrity sha512-MsIgYmdBh460ZZ8cJC81q4XJknjG567wzEmv46WOBblDb6TUd3z8/GhgmsM9pn8g2B80tAJ4m5/d3Bi1KrSUBQ== hexoid@^1.0.0: version "1.0.0" @@ -2196,6 +2246,13 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ignore-by-default@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" @@ -2235,7 +2292,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@~2.0.3: +inherits@2, inherits@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2262,10 +2319,10 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-core-module@^2.9.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" - integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== +is-core-module@^2.11.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" + integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== dependencies: has "^1.0.3" @@ -2301,6 +2358,11 @@ is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-property@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g== + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -2311,11 +2373,6 @@ is_js@^0.9.0: resolved "https://registry.yarnpkg.com/is_js/-/is_js-0.9.0.tgz#0ab94540502ba7afa24c856aa985561669e9c52d" integrity sha512-8Y5EHSH+TonfUHX2g3pMJljdbGavg55q4jmHzghJCdqYDbdNROC8uw/YFQwIRCRqRJT1EY3pJefz+kglw+o7sg== -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -2363,284 +2420,284 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jest-changed-files@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.4.2.tgz#bee1fafc8b620d6251423d1978a0080546bc4376" - integrity sha512-Qdd+AXdqD16PQa+VsWJpxR3kN0JyOCX1iugQfx5nUgAsI4gwsKviXkpclxOK9ZnwaY2IQVHz+771eAvqeOlfuw== +jest-changed-files@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.5.0.tgz#e88786dca8bf2aa899ec4af7644e16d9dcf9b23e" + integrity sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag== dependencies: execa "^5.0.0" p-limit "^3.1.0" -jest-circus@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.4.2.tgz#2d00c04baefd0ee2a277014cd494d4b5970663ed" - integrity sha512-wW3ztp6a2P5c1yOc1Cfrt5ozJ7neWmqeXm/4SYiqcSriyisgq63bwFj1NuRdSR5iqS0CMEYwSZd89ZA47W9zUg== +jest-circus@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.5.0.tgz#b5926989449e75bff0d59944bae083c9d7fb7317" + integrity sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA== dependencies: - "@jest/environment" "^29.4.2" - "@jest/expect" "^29.4.2" - "@jest/test-result" "^29.4.2" - "@jest/types" "^29.4.2" + "@jest/environment" "^29.5.0" + "@jest/expect" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/types" "^29.5.0" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" dedent "^0.7.0" is-generator-fn "^2.0.0" - jest-each "^29.4.2" - jest-matcher-utils "^29.4.2" - jest-message-util "^29.4.2" - jest-runtime "^29.4.2" - jest-snapshot "^29.4.2" - jest-util "^29.4.2" + jest-each "^29.5.0" + jest-matcher-utils "^29.5.0" + jest-message-util "^29.5.0" + jest-runtime "^29.5.0" + jest-snapshot "^29.5.0" + jest-util "^29.5.0" p-limit "^3.1.0" - pretty-format "^29.4.2" + pretty-format "^29.5.0" + pure-rand "^6.0.0" slash "^3.0.0" stack-utils "^2.0.3" -jest-cli@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.4.2.tgz#94a2f913a0a7a49d11bee98ad88bf48baae941f4" - integrity sha512-b+eGUtXq/K2v7SH3QcJvFvaUaCDS1/YAZBYz0m28Q/Ppyr+1qNaHmVYikOrbHVbZqYQs2IeI3p76uy6BWbXq8Q== +jest-cli@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.5.0.tgz#b34c20a6d35968f3ee47a7437ff8e53e086b4a67" + integrity sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw== dependencies: - "@jest/core" "^29.4.2" - "@jest/test-result" "^29.4.2" - "@jest/types" "^29.4.2" + "@jest/core" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/types" "^29.5.0" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.9" import-local "^3.0.2" - jest-config "^29.4.2" - jest-util "^29.4.2" - jest-validate "^29.4.2" + jest-config "^29.5.0" + jest-util "^29.5.0" + jest-validate "^29.5.0" prompts "^2.0.1" yargs "^17.3.1" -jest-config@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.4.2.tgz#15386dd9ed2f7059516915515f786b8836a98f07" - integrity sha512-919CtnXic52YM0zW4C1QxjG6aNueX1kBGthuMtvFtRTAxhKfJmiXC9qwHmi6o2josjbDz8QlWyY55F1SIVmCWA== +jest-config@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.5.0.tgz#3cc972faec8c8aaea9ae158c694541b79f3748da" + integrity sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA== dependencies: "@babel/core" "^7.11.6" - "@jest/test-sequencer" "^29.4.2" - "@jest/types" "^29.4.2" - babel-jest "^29.4.2" + "@jest/test-sequencer" "^29.5.0" + "@jest/types" "^29.5.0" + babel-jest "^29.5.0" chalk "^4.0.0" ci-info "^3.2.0" deepmerge "^4.2.2" glob "^7.1.3" graceful-fs "^4.2.9" - jest-circus "^29.4.2" - jest-environment-node "^29.4.2" - jest-get-type "^29.4.2" - jest-regex-util "^29.4.2" - jest-resolve "^29.4.2" - jest-runner "^29.4.2" - jest-util "^29.4.2" - jest-validate "^29.4.2" + jest-circus "^29.5.0" + jest-environment-node "^29.5.0" + jest-get-type "^29.4.3" + jest-regex-util "^29.4.3" + jest-resolve "^29.5.0" + jest-runner "^29.5.0" + jest-util "^29.5.0" + jest-validate "^29.5.0" micromatch "^4.0.4" parse-json "^5.2.0" - pretty-format "^29.4.2" + pretty-format "^29.5.0" slash "^3.0.0" strip-json-comments "^3.1.1" -jest-diff@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.4.2.tgz#b88502d5dc02d97f6512d73c37da8b36f49b4871" - integrity sha512-EK8DSajVtnjx9sa1BkjZq3mqChm2Cd8rIzdXkQMA8e0wuXq53ypz6s5o5V8HRZkoEt2ywJ3eeNWFKWeYr8HK4g== +jest-diff@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.5.0.tgz#e0d83a58eb5451dcc1fa61b1c3ee4e8f5a290d63" + integrity sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw== dependencies: chalk "^4.0.0" - diff-sequences "^29.4.2" - jest-get-type "^29.4.2" - pretty-format "^29.4.2" + diff-sequences "^29.4.3" + jest-get-type "^29.4.3" + pretty-format "^29.5.0" -jest-docblock@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.2.tgz#c78a95eedf9a24c0a6cc16cf2abdc4b8b0f2531b" - integrity sha512-dV2JdahgClL34Y5vLrAHde3nF3yo2jKRH+GIYJuCpfqwEJZcikzeafVTGAjbOfKPG17ez9iWXwUYp7yefeCRag== +jest-docblock@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.3.tgz#90505aa89514a1c7dceeac1123df79e414636ea8" + integrity sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg== dependencies: detect-newline "^3.0.0" -jest-each@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.4.2.tgz#e1347aff1303f4c35470827a62c029d389c5d44a" - integrity sha512-trvKZb0JYiCndc55V1Yh0Luqi7AsAdDWpV+mKT/5vkpnnFQfuQACV72IoRV161aAr6kAVIBpmYzwhBzm34vQkA== +jest-each@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.5.0.tgz#fc6e7014f83eac68e22b7195598de8554c2e5c06" + integrity sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA== dependencies: - "@jest/types" "^29.4.2" + "@jest/types" "^29.5.0" chalk "^4.0.0" - jest-get-type "^29.4.2" - jest-util "^29.4.2" - pretty-format "^29.4.2" - -jest-environment-node@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.4.2.tgz#0eab835b41e25fd0c1a72f62665fc8db08762ad2" - integrity sha512-MLPrqUcOnNBc8zTOfqBbxtoa8/Ee8tZ7UFW7hRDQSUT+NGsvS96wlbHGTf+EFAT9KC3VNb7fWEM6oyvmxtE/9w== - dependencies: - "@jest/environment" "^29.4.2" - "@jest/fake-timers" "^29.4.2" - "@jest/types" "^29.4.2" + jest-get-type "^29.4.3" + jest-util "^29.5.0" + pretty-format "^29.5.0" + +jest-environment-node@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.5.0.tgz#f17219d0f0cc0e68e0727c58b792c040e332c967" + integrity sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw== + dependencies: + "@jest/environment" "^29.5.0" + "@jest/fake-timers" "^29.5.0" + "@jest/types" "^29.5.0" "@types/node" "*" - jest-mock "^29.4.2" - jest-util "^29.4.2" + jest-mock "^29.5.0" + jest-util "^29.5.0" -jest-get-type@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.2.tgz#7cb63f154bca8d8f57364d01614477d466fa43fe" - integrity sha512-vERN30V5i2N6lqlFu4ljdTqQAgrkTFMC9xaIIfOPYBw04pufjXRty5RuXBiB1d72tGbURa/UgoiHB90ruOSivg== +jest-get-type@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5" + integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg== -jest-haste-map@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.4.2.tgz#9112df3f5121e643f1b2dcbaa86ab11b0b90b49a" - integrity sha512-WkUgo26LN5UHPknkezrBzr7lUtV1OpGsp+NfXbBwHztsFruS3gz+AMTTBcEklvi8uPzpISzYjdKXYZQJXBnfvw== +jest-haste-map@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.5.0.tgz#69bd67dc9012d6e2723f20a945099e972b2e94de" + integrity sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA== dependencies: - "@jest/types" "^29.4.2" + "@jest/types" "^29.5.0" "@types/graceful-fs" "^4.1.3" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" graceful-fs "^4.2.9" - jest-regex-util "^29.4.2" - jest-util "^29.4.2" - jest-worker "^29.4.2" + jest-regex-util "^29.4.3" + jest-util "^29.5.0" + jest-worker "^29.5.0" micromatch "^4.0.4" walker "^1.0.8" optionalDependencies: fsevents "^2.3.2" -jest-leak-detector@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.4.2.tgz#8f05c6680e0cb46a1d577c0d3da9793bed3ea97b" - integrity sha512-Wa62HuRJmWXtX9F00nUpWlrbaH5axeYCdyRsOs/+Rb1Vb6+qWTlB5rKwCCRKtorM7owNwKsyJ8NRDUcZ8ghYUA== +jest-leak-detector@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz#cf4bdea9615c72bac4a3a7ba7e7930f9c0610c8c" + integrity sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow== dependencies: - jest-get-type "^29.4.2" - pretty-format "^29.4.2" + jest-get-type "^29.4.3" + pretty-format "^29.5.0" -jest-matcher-utils@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.4.2.tgz#08d0bf5abf242e3834bec92c7ef5071732839e85" - integrity sha512-EZaAQy2je6Uqkrm6frnxBIdaWtSYFoR8SVb2sNLAtldswlR/29JAgx+hy67llT3+hXBaLB0zAm5UfeqerioZyg== +jest-matcher-utils@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz#d957af7f8c0692c5453666705621ad4abc2c59c5" + integrity sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw== dependencies: chalk "^4.0.0" - jest-diff "^29.4.2" - jest-get-type "^29.4.2" - pretty-format "^29.4.2" + jest-diff "^29.5.0" + jest-get-type "^29.4.3" + pretty-format "^29.5.0" -jest-message-util@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.4.2.tgz#309a2924eae6ca67cf7f25781a2af1902deee717" - integrity sha512-SElcuN4s6PNKpOEtTInjOAA8QvItu0iugkXqhYyguRvQoXapg5gN+9RQxLAkakChZA7Y26j6yUCsFWN+hlKD6g== +jest-message-util@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.5.0.tgz#1f776cac3aca332ab8dd2e3b41625435085c900e" + integrity sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA== dependencies: "@babel/code-frame" "^7.12.13" - "@jest/types" "^29.4.2" + "@jest/types" "^29.5.0" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" graceful-fs "^4.2.9" micromatch "^4.0.4" - pretty-format "^29.4.2" + pretty-format "^29.5.0" slash "^3.0.0" stack-utils "^2.0.3" -jest-mock@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.4.2.tgz#e1054be66fb3e975d26d4528fcde6979e4759de8" - integrity sha512-x1FSd4Gvx2yIahdaIKoBjwji6XpboDunSJ95RpntGrYulI1ByuYQCKN/P7hvk09JB74IonU3IPLdkutEWYt++g== +jest-mock@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.5.0.tgz#26e2172bcc71d8b0195081ff1f146ac7e1518aed" + integrity sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw== dependencies: - "@jest/types" "^29.4.2" + "@jest/types" "^29.5.0" "@types/node" "*" - jest-util "^29.4.2" + jest-util "^29.5.0" jest-pnp-resolver@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== -jest-regex-util@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.2.tgz#19187cca35d301f8126cf7a021dd4dcb7b58a1ca" - integrity sha512-XYZXOqUl1y31H6VLMrrUL1ZhXuiymLKPz0BO1kEeR5xER9Tv86RZrjTm74g5l9bPJQXA/hyLdaVPN/sdqfteig== +jest-regex-util@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.3.tgz#a42616141e0cae052cfa32c169945d00c0aa0bb8" + integrity sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg== -jest-resolve-dependencies@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.4.2.tgz#6359db606f5967b68ca8bbe9dbc07a4306c12bf7" - integrity sha512-6pL4ptFw62rjdrPk7rRpzJYgcRqRZNsZTF1VxVTZMishbO6ObyWvX57yHOaNGgKoADtAHRFYdHQUEvYMJATbDg== +jest-resolve-dependencies@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz#f0ea29955996f49788bf70996052aa98e7befee4" + integrity sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg== dependencies: - jest-regex-util "^29.4.2" - jest-snapshot "^29.4.2" + jest-regex-util "^29.4.3" + jest-snapshot "^29.5.0" -jest-resolve@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.4.2.tgz#8831f449671d08d161fe493003f61dc9b55b808e" - integrity sha512-RtKWW0mbR3I4UdkOrW7552IFGLYQ5AF9YrzD0FnIOkDu0rAMlA5/Y1+r7lhCAP4nXSBTaE7ueeqj6IOwZpgoqw== +jest-resolve@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.5.0.tgz#b053cc95ad1d5f6327f0ac8aae9f98795475ecdc" + integrity sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w== dependencies: chalk "^4.0.0" graceful-fs "^4.2.9" - jest-haste-map "^29.4.2" + jest-haste-map "^29.5.0" jest-pnp-resolver "^1.2.2" - jest-util "^29.4.2" - jest-validate "^29.4.2" + jest-util "^29.5.0" + jest-validate "^29.5.0" resolve "^1.20.0" resolve.exports "^2.0.0" slash "^3.0.0" -jest-runner@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.4.2.tgz#2bcecf72303369df4ef1e6e983c22a89870d5125" - integrity sha512-wqwt0drm7JGjwdH+x1XgAl+TFPH7poowMguPQINYxaukCqlczAcNLJiK+OLxUxQAEWMdy+e6nHZlFHO5s7EuRg== +jest-runner@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.5.0.tgz#6a57c282eb0ef749778d444c1d758c6a7693b6f8" + integrity sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ== dependencies: - "@jest/console" "^29.4.2" - "@jest/environment" "^29.4.2" - "@jest/test-result" "^29.4.2" - "@jest/transform" "^29.4.2" - "@jest/types" "^29.4.2" + "@jest/console" "^29.5.0" + "@jest/environment" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" "@types/node" "*" chalk "^4.0.0" emittery "^0.13.1" graceful-fs "^4.2.9" - jest-docblock "^29.4.2" - jest-environment-node "^29.4.2" - jest-haste-map "^29.4.2" - jest-leak-detector "^29.4.2" - jest-message-util "^29.4.2" - jest-resolve "^29.4.2" - jest-runtime "^29.4.2" - jest-util "^29.4.2" - jest-watcher "^29.4.2" - jest-worker "^29.4.2" + jest-docblock "^29.4.3" + jest-environment-node "^29.5.0" + jest-haste-map "^29.5.0" + jest-leak-detector "^29.5.0" + jest-message-util "^29.5.0" + jest-resolve "^29.5.0" + jest-runtime "^29.5.0" + jest-util "^29.5.0" + jest-watcher "^29.5.0" + jest-worker "^29.5.0" p-limit "^3.1.0" source-map-support "0.5.13" -jest-runtime@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.4.2.tgz#d86b764c5b95d76cb26ed1f32644e99de5d5c134" - integrity sha512-3fque9vtpLzGuxT9eZqhxi+9EylKK/ESfhClv4P7Y9sqJPs58LjVhTt8jaMp/pRO38agll1CkSu9z9ieTQeRrw== - dependencies: - "@jest/environment" "^29.4.2" - "@jest/fake-timers" "^29.4.2" - "@jest/globals" "^29.4.2" - "@jest/source-map" "^29.4.2" - "@jest/test-result" "^29.4.2" - "@jest/transform" "^29.4.2" - "@jest/types" "^29.4.2" +jest-runtime@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.5.0.tgz#c83f943ee0c1da7eb91fa181b0811ebd59b03420" + integrity sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw== + dependencies: + "@jest/environment" "^29.5.0" + "@jest/fake-timers" "^29.5.0" + "@jest/globals" "^29.5.0" + "@jest/source-map" "^29.4.3" + "@jest/test-result" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" "@types/node" "*" chalk "^4.0.0" cjs-module-lexer "^1.0.0" collect-v8-coverage "^1.0.0" glob "^7.1.3" graceful-fs "^4.2.9" - jest-haste-map "^29.4.2" - jest-message-util "^29.4.2" - jest-mock "^29.4.2" - jest-regex-util "^29.4.2" - jest-resolve "^29.4.2" - jest-snapshot "^29.4.2" - jest-util "^29.4.2" - semver "^7.3.5" + jest-haste-map "^29.5.0" + jest-message-util "^29.5.0" + jest-mock "^29.5.0" + jest-regex-util "^29.4.3" + jest-resolve "^29.5.0" + jest-snapshot "^29.5.0" + jest-util "^29.5.0" slash "^3.0.0" strip-bom "^4.0.0" -jest-snapshot@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.4.2.tgz#ba1fb9abb279fd2c85109ff1757bc56b503bbb3a" - integrity sha512-PdfubrSNN5KwroyMH158R23tWcAXJyx4pvSvWls1dHoLCaUhGul9rsL3uVjtqzRpkxlkMavQjGuWG1newPgmkw== +jest-snapshot@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.5.0.tgz#c9c1ce0331e5b63cd444e2f95a55a73b84b1e8ce" + integrity sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g== dependencies: "@babel/core" "^7.11.6" "@babel/generator" "^7.7.2" @@ -2648,87 +2705,81 @@ jest-snapshot@^29.4.2: "@babel/plugin-syntax-typescript" "^7.7.2" "@babel/traverse" "^7.7.2" "@babel/types" "^7.3.3" - "@jest/expect-utils" "^29.4.2" - "@jest/transform" "^29.4.2" - "@jest/types" "^29.4.2" + "@jest/expect-utils" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" "@types/babel__traverse" "^7.0.6" "@types/prettier" "^2.1.5" babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^29.4.2" + expect "^29.5.0" graceful-fs "^4.2.9" - jest-diff "^29.4.2" - jest-get-type "^29.4.2" - jest-haste-map "^29.4.2" - jest-matcher-utils "^29.4.2" - jest-message-util "^29.4.2" - jest-util "^29.4.2" + jest-diff "^29.5.0" + jest-get-type "^29.4.3" + jest-matcher-utils "^29.5.0" + jest-message-util "^29.5.0" + jest-util "^29.5.0" natural-compare "^1.4.0" - pretty-format "^29.4.2" + pretty-format "^29.5.0" semver "^7.3.5" -jest-util@^29.0.0, jest-util@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.4.2.tgz#3db8580b295df453a97de4a1b42dd2578dabd2c2" - integrity sha512-wKnm6XpJgzMUSRFB7YF48CuwdzuDIHenVuoIb1PLuJ6F+uErZsuDkU+EiExkChf6473XcawBrSfDSnXl+/YG4g== +jest-util@^29.0.0, jest-util@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.5.0.tgz#24a4d3d92fc39ce90425311b23c27a6e0ef16b8f" + integrity sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ== dependencies: - "@jest/types" "^29.4.2" + "@jest/types" "^29.5.0" "@types/node" "*" chalk "^4.0.0" ci-info "^3.2.0" graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-validate@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.4.2.tgz#3b3f8c4910ab9a3442d2512e2175df6b3f77b915" - integrity sha512-tto7YKGPJyFbhcKhIDFq8B5od+eVWD/ySZ9Tvcp/NGCvYA4RQbuzhbwYWtIjMT5W5zA2W0eBJwu4HVw34d5G6Q== +jest-validate@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.5.0.tgz#8e5a8f36178d40e47138dc00866a5f3bd9916ffc" + integrity sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ== dependencies: - "@jest/types" "^29.4.2" + "@jest/types" "^29.5.0" camelcase "^6.2.0" chalk "^4.0.0" - jest-get-type "^29.4.2" + jest-get-type "^29.4.3" leven "^3.1.0" - pretty-format "^29.4.2" + pretty-format "^29.5.0" -jest-watcher@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.4.2.tgz#09c0f4c9a9c7c0807fcefb1445b821c6f7953b7c" - integrity sha512-onddLujSoGiMJt+tKutehIidABa175i/Ays+QvKxCqBwp7fvxP3ZhKsrIdOodt71dKxqk4sc0LN41mWLGIK44w== +jest-watcher@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.5.0.tgz#cf7f0f949828ba65ddbbb45c743a382a4d911363" + integrity sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA== dependencies: - "@jest/test-result" "^29.4.2" - "@jest/types" "^29.4.2" + "@jest/test-result" "^29.5.0" + "@jest/types" "^29.5.0" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" emittery "^0.13.1" - jest-util "^29.4.2" + jest-util "^29.5.0" string-length "^4.0.1" -jest-worker@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.4.2.tgz#d9b2c3bafc69311d84d94e7fb45677fc8976296f" - integrity sha512-VIuZA2hZmFyRbchsUCHEehoSf2HEl0YVF8SDJqtPnKorAaBuh42V8QsLnde0XP5F6TyCynGPEGgBOn3Fc+wZGw== +jest-worker@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.5.0.tgz#bdaefb06811bd3384d93f009755014d8acb4615d" + integrity sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA== dependencies: "@types/node" "*" - jest-util "^29.4.2" + jest-util "^29.5.0" merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest/-/jest-29.4.2.tgz#4c2127d03a71dc187f386156ef155dbf323fb7be" - integrity sha512-+5hLd260vNIHu+7ZgMIooSpKl7Jp5pHKb51e73AJU3owd5dEo/RfVwHbA/na3C/eozrt3hJOLGf96c7EWwIAzg== +jest@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.5.0.tgz#f75157622f5ce7ad53028f2f8888ab53e1f1f24e" + integrity sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ== dependencies: - "@jest/core" "^29.4.2" - "@jest/types" "^29.4.2" + "@jest/core" "^29.5.0" + "@jest/types" "^29.5.0" import-local "^3.0.2" - jest-cli "^29.4.2" - -js-sdsl@^4.1.4: - version "4.3.0" - resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" - integrity sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ== + jest-cli "^29.5.0" js-tokens@^4.0.0: version "4.0.0" @@ -2852,6 +2903,11 @@ lodash@^4.14.2, lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +long@^5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -2866,15 +2922,25 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + +lru-cache@^8.0.0: + version "8.0.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-8.0.5.tgz#983fe337f3e176667f8e567cfcce7cb064ea214e" + integrity sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA== + lru-cache@~2.2.1: version "2.2.4" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d" integrity sha512-Q5pAgXs+WEAfoEdw2qKQhNFFhMoFMTYqRVKKUMnzuiR7oKFHS7fWo848cPcTKw+4j/IdN17NyzdhVKgabFV0EA== -luxon@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.2.1.tgz#14f1af209188ad61212578ea7e3d518d18cee45f" - integrity sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg== +luxon@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.3.0.tgz#d73ab5b5d2b49a461c47cedbc7e73309b4805b48" + integrity sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg== make-dir@^3.0.0: version "3.1.0" @@ -2986,15 +3052,26 @@ ms@2.1.3, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -mysql@^2.18.1: - version "2.18.1" - resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717" - integrity sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig== +mysql2@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.3.1.tgz#b7e055812adeb46d96bc65a5b37fa57e70bc0388" + integrity sha512-UD84/AvLwO5qmSABEsBTZ7y7JKv3sM8JzWGhuL4tDkJwVsClVVAcelNSR5Unyhxj6/KHBAkjS7qe5/c+gEmNvA== + dependencies: + denque "^2.1.0" + generate-function "^2.3.1" + iconv-lite "^0.6.3" + long "^5.2.1" + lru-cache "^8.0.0" + named-placeholders "^1.1.3" + seq-queue "^0.0.5" + sqlstring "^2.3.2" + +named-placeholders@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351" + integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w== dependencies: - bignumber.js "9.0.0" - readable-stream "2.3.7" - safe-buffer "5.1.2" - sqlstring "2.3.1" + lru-cache "^7.14.1" natural-compare-lite@^1.4.0: version "1.4.0" @@ -3017,14 +3094,14 @@ node-int64@^0.4.0: integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== node-releases@^2.0.8: - version "2.0.10" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" - integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== + version "2.0.11" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.11.tgz#59d7cef999d13f908e43b5a70001cf3129542f0f" + integrity sha512-+M0PwXeU80kRohZ3aT4J/OnR+l9/KD2nVLNNoRgFtnf+umQVFdGBAO2N8+nCnEi0xlh/Wk3zOGC+vNNx+uM79Q== -nodemon@^2.0.15: - version "2.0.20" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.20.tgz#e3537de768a492e8d74da5c5813cb0c7486fc701" - integrity sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw== +nodemon@^2.0.22: + version "2.0.22" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.22.tgz#182c45c3a78da486f673d6c1702e00728daf5258" + integrity sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ== dependencies: chokidar "^3.5.2" debug "^3.2.7" @@ -3221,25 +3298,20 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@^2.8.4: - version "2.8.4" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3" - integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw== +prettier@^2.8.8: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== -pretty-format@^29.0.0, pretty-format@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.4.2.tgz#64bf5ccc0d718c03027d94ac957bdd32b3fb2401" - integrity sha512-qKlHR8yFVCbcEWba0H0TOC8dnLlO4vPlyEjRPw31FZ2Rupy9nLa8ZLbYny8gWEl8CkEhJqAE6IzdNELTBVcBEg== +pretty-format@^29.0.0, pretty-format@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.5.0.tgz#283134e74f70e2e3e7229336de0e4fce94ccde5a" + integrity sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw== dependencies: - "@jest/schemas" "^29.4.2" + "@jest/schemas" "^29.4.3" ansi-styles "^5.0.0" react-is "^18.0.0" -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -3266,13 +3338,25 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== -qs@6.11.0, qs@^6.11.0: +pure-rand@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306" + integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ== + +qs@6.11.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== dependencies: side-channel "^1.0.4" +qs@^6.11.0: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -3303,19 +3387,6 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -readable-stream@2.3.7: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -3330,37 +3401,17 @@ rechoir@^0.8.0: dependencies: resolve "^1.20.0" -redis-commands@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" - integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ== - -redis-errors@^1.0.0, redis-errors@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" - integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== - -redis-parser@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" - integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== +redis@^4.6.6: + version "4.6.6" + resolved "https://registry.yarnpkg.com/redis/-/redis-4.6.6.tgz#46d4f2d149d1634d6ef53db5747412a0ef7974ec" + integrity sha512-aLs2fuBFV/VJ28oLBqYykfnhGGkFxvx0HdCEBYdJ99FFbSEMZ7c1nVKwR6ZRv+7bb7JnC0mmCzaqu8frgOYhpA== dependencies: - redis-errors "^1.0.0" - -redis@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/redis/-/redis-3.1.2.tgz#766851117e80653d23e0ed536254677ab647638c" - integrity sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw== - dependencies: - denque "^1.5.0" - redis-commands "^1.7.0" - redis-errors "^1.2.0" - redis-parser "^3.0.0" - -regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + "@redis/bloom" "1.2.0" + "@redis/client" "1.5.7" + "@redis/graph" "1.1.0" + "@redis/json" "1.0.4" + "@redis/search" "1.1.2" + "@redis/time-series" "1.0.4" request-ip@~2.0.1: version "2.0.2" @@ -3392,16 +3443,16 @@ resolve-from@^5.0.0: integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve.exports@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.0.tgz#c1a0028c2d166ec2fbf7d0644584927e76e7400e" - integrity sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg== + version "2.0.2" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" + integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== resolve@^1.20.0: - version "1.22.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + version "1.22.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" + integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== dependencies: - is-core-module "^2.9.0" + is-core-module "^2.11.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -3439,25 +3490,20 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - safe-buffer@5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -"safer-buffer@>= 2.1.2 < 3": +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== semver@7.x, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: - version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + version "7.5.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec" + integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw== dependencies: lru-cache "^6.0.0" @@ -3495,6 +3541,11 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" +seq-queue@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e" + integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q== + serve-static@1.15.0: version "1.15.0" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" @@ -3561,22 +3612,22 @@ socket.io-adapter@~2.5.2: ws "~8.11.0" socket.io-parser@~4.2.1: - version "4.2.2" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206" - integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw== + version "4.2.3" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.3.tgz#926bcc6658e2ae0883dc9dee69acbdc76e4e3667" + integrity sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" -socket.io@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.6.0.tgz#82ebfd7652572872e10dbb19533fc7cb930d0bc3" - integrity sha512-b65bp6INPk/BMMrIgVvX12x3Q+NqlGqSlTuvKQWt0BUJ3Hyy3JangBl7fEoWZTXbOKlCqNPbQ6MbWgok/km28w== +socket.io@^4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.6.1.tgz#62ec117e5fce0692fa50498da9347cfb52c3bc70" + integrity sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA== dependencies: accepts "~1.3.4" base64id "~2.0.0" debug "~4.3.2" - engine.io "~6.4.0" + engine.io "~6.4.1" socket.io-adapter "~2.5.2" socket.io-parser "~4.2.1" @@ -3603,10 +3654,10 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -sqlstring@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40" - integrity sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ== +sqlstring@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c" + integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg== stack-utils@^2.0.3: version "2.0.6" @@ -3642,13 +3693,6 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -3779,10 +3823,10 @@ traverse-chain@~0.1.0: resolved "https://registry.yarnpkg.com/traverse-chain/-/traverse-chain-0.1.0.tgz#61dbc2d53b69ff6091a12a168fd7d433107e40f1" integrity sha512-up6Yvai4PYKhpNp5PkYtx50m3KbwQrqDwbuZP/ItyL64YEWHAvH6Md83LFLV/GRSk/BoUVwwgUzX6SOQSbsfAg== -ts-jest@^29.0.5: - version "29.0.5" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.0.5.tgz#c5557dcec8fe434fcb8b70c3e21c6b143bfce066" - integrity sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA== +ts-jest@^29.1.0: + version "29.1.0" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.0.tgz#4a9db4104a49b76d2b368ea775b6c9535c603891" + integrity sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA== dependencies: bs-logger "0.x" fast-json-stable-stringify "2.x" @@ -3854,10 +3898,10 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typescript@^4.9.5: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== uid-safe@~2.1.5: version "2.1.5" @@ -3877,9 +3921,9 @@ unpipe@1.0.0, unpipe@~1.0.0: integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== update-browserslist-db@^1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" - integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== dependencies: escalade "^3.1.1" picocolors "^1.0.0" @@ -3891,11 +3935,6 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" @@ -3907,9 +3946,9 @@ v8-compile-cache-lib@^3.0.1: integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== v8-to-istanbul@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4" - integrity sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w== + version "9.1.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265" + integrity sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA== dependencies: "@jridgewell/trace-mapping" "^0.3.12" "@types/istanbul-lib-coverage" "^2.0.1" @@ -3971,25 +4010,25 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yallist@4.0.0, yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== yargs@^17.3.1: - version "17.6.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" - integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: cliui "^8.0.1" escalade "^3.1.1" diff --git a/db/migration/V021__create_assessments.sql b/db/migration/V021__create_assessments.sql new file mode 100644 index 00000000..9183db29 --- /dev/null +++ b/db/migration/V021__create_assessments.sql @@ -0,0 +1,155 @@ +CREATE TABLE assessment_question_types ( + id BIGINT NOT NULL AUTO_INCREMENT, + title TEXT NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE assessment_submission_states ( + id BIGINT NOT NULL AUTO_INCREMENT, + title TEXT NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE program_participant_roles ( + id BIGINT NOT NULL AUTO_INCREMENT, + title TEXT NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE program_participants ( + id BIGINT NOT NULL AUTO_INCREMENT, + principal_id BIGINT NOT NULL, + program_id BIGINT NOT NULL, + role_id BIGINT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX program_participants_principal_id (principal_id), + FOREIGN KEY (principal_id) REFERENCES principals(id), + INDEX program_participants_program_id (program_id), + FOREIGN KEY (program_id) REFERENCES programs(id) ON DELETE CASCADE, + INDEX program_participants_role_id (role_id), + FOREIGN KEY (role_id) REFERENCES program_participant_roles(id) ON DELETE SET NULL, + PRIMARY KEY (id), + UNIQUE KEY `principals_and_programs` (`principal_id`,`program_id`) +); + +CREATE TABLE curriculum_assessments ( + id BIGINT NOT NULL AUTO_INCREMENT, + title TEXT NOT NULL, + description TEXT NOT NULL, + max_score INT, + max_num_submissions INT NOT NULL DEFAULT 1, + time_limit INT, + curriculum_id BIGINT NOT NULL, + activity_id BIGINT NULL, + principal_id BIGINT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX curriculum_assessments_curriculum_id (curriculum_id), + FOREIGN KEY (curriculum_id) REFERENCES curriculums(id) ON DELETE CASCADE, + INDEX curriculum_assessments_activity_id (activity_id), + FOREIGN KEY (activity_id) REFERENCES activities(id) ON DELETE SET NULL, + INDEX curriculum_assessments_principal_id (principal_id), + FOREIGN KEY (principal_id) REFERENCES principals(id), + PRIMARY KEY (id) +); + +CREATE TABLE program_assessments ( + id BIGINT NOT NULL AUTO_INCREMENT, + program_id BIGINT NOT NULL, + assessment_id BIGINT NOT NULL, + available_after TIMESTAMP NOT NULL, + due_date TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX program_assessments_program_id (program_id), + FOREIGN KEY (program_id) REFERENCES programs(id) ON DELETE CASCADE, + INDEX program_assessments_assessment_id (assessment_id), + FOREIGN KEY (assessment_id) REFERENCES curriculum_assessments(id) ON DELETE CASCADE, + PRIMARY KEY (id), + UNIQUE KEY `program_and_assessment` (`program_id`,`assessment_id`) +); + +CREATE TABLE assessment_questions ( + id BIGINT NOT NULL AUTO_INCREMENT, + assessment_id BIGINT NOT NULL, + title TEXT NOT NULL, + description TEXT, + question_type_id BIGINT NOT NULL, + correct_answer_id BIGINT, + max_score INT, + sort_order INT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX assessment_answers_assessment_question_id (assessment_id), + FOREIGN KEY (assessment_id) REFERENCES curriculum_assessments(id) ON DELETE CASCADE, + INDEX assessment_questions_question_type_id (question_type_id), + FOREIGN KEY (question_type_id) REFERENCES assessment_question_types(id), + PRIMARY KEY (id), + UNIQUE KEY `assessment_and_question_number` (`assessment_id`,`sort_order`) +); + +CREATE TABLE assessment_answers ( + id BIGINT NOT NULL AUTO_INCREMENT, + question_id BIGINT, + title TEXT NOT NULL, + description TEXT, + sort_order INT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX assessment_answers_question_id (question_id), + FOREIGN KEY (question_id) REFERENCES assessment_questions(id) ON DELETE CASCADE, + PRIMARY KEY (id), + UNIQUE KEY `question_and_answer_order` (`question_id`,`sort_order`) +); + +CREATE TABLE assessment_submissions ( + id BIGINT NOT NULL AUTO_INCREMENT, + assessment_id BIGINT NOT NULL, + principal_id BIGINT NOT NULL, + assessment_submission_state_id BIGINT NOT NULL, + score INT, + opened_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + submitted_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX assessment_submissions_assessment_id (assessment_id), + FOREIGN KEY (assessment_id) REFERENCES program_assessments(id), + INDEX assessment_submissions_principal_id (principal_id), + FOREIGN KEY (principal_id) REFERENCES principals(id), + INDEX assessment_submissions_assessment_submission_state_id (assessment_submission_state_id), + FOREIGN KEY (assessment_submission_state_id) REFERENCES assessment_submission_states(id), + PRIMARY KEY (id) +); + +CREATE TABLE assessment_responses ( + id BIGINT NOT NULL AUTO_INCREMENT, + assessment_id BIGINT NOT NULL, + submission_id BIGINT NOT NULL, + question_id BIGINT NOT NULL, + answer_id BIGINT, + response TEXT, + score INT, + grader_response TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX assessment_responses_assessment_id (assessment_id), + FOREIGN KEY (assessment_id) REFERENCES program_assessments(id), + INDEX assessment_responses_submission_id (submission_id), + FOREIGN KEY (submission_id) REFERENCES assessment_submissions(id) ON DELETE CASCADE, + INDEX assessment_responses_question_id (question_id), + FOREIGN KEY (question_id) REFERENCES assessment_questions(id), + INDEX assessment_responses_answer_id (answer_id), + FOREIGN KEY (answer_id) REFERENCES assessment_answers(id) ON DELETE SET NULL, + PRIMARY KEY (id), + UNIQUE KEY `submission_and_question` (`submission_id`,`question_id`) +); + +ALTER TABLE questionnaires +RENAME TO surveys; + +ALTER TABLE prompts +RENAME TO survey_questions; + +ALTER TABLE options +RENAME TO survey_answers; diff --git a/db/migration/V022__insert_sample_assessment_data.sql b/db/migration/V022__insert_sample_assessment_data.sql new file mode 100644 index 00000000..834cc5a5 --- /dev/null +++ b/db/migration/V022__insert_sample_assessment_data.sql @@ -0,0 +1,163 @@ +INSERT INTO `activity_types` + (`title`) +VALUES + ("quiz"), + ("test"); + +INSERT INTO `assessment_submission_states` + (`title`) +VALUES + ("Inactive"), + ("Active"), + ("Opened"), + ("In Progress"), + ("Expired"), + ("Submitted"), + ("Graded"), + ("Hidden"); + +INSERT INTO `program_participant_roles` + (`title`) +VALUES + ("Facilitator"), + ("Participant"); + +INSERT INTO `curriculums` + (`title`, `principal_id`) +VALUES + ("Beginner Web Development", 2); + +INSERT INTO `programs` + (`title`, `start_date`, `end_date`, `time_zone`, `principal_id`, `curriculum_id`) +VALUES + ("Cohort 5", "2023-02-06", "2023-03-31", "America/Vancouver", 2, 3); + +INSERT INTO `activities` + (`title`, `description_text`, `curriculum_week`, `curriculum_day`, `start_time`, `end_time`, `duration`, `activity_type_id`, `curriculum_id`) +VALUES + ("Assignment 1: React", "Your assignment for week 1 learning.", 1, 5, NULL, NULL, NULL, 1, 3), + ("SQL Quiz", "A check on your SQL learning.", 7, 5, NULL, NULL, NULL, 10, 3), + ("Final Exam", "The final exam for the course.", 8, 5, NULL, NULL, NULL, 11, 3); + +INSERT INTO `assessment_question_types` + (`title`) +VALUES + ("single choice"), + ("free response"); + +INSERT INTO `curriculum_assessments` + (`title`, `description`, `max_score`, `max_num_submissions`, `time_limit`, `curriculum_id`, `activity_id`, `principal_id`) +VALUES + ("Assignment 1: React", "Your assignment for week 1 learning.", 10, 1, NULL, 3, 97, 2), + ("SQL Quiz", "A check on your SQL learning.", 5, 3, NULL, 3, 98, 2), + ("Final Exam", "The final exam for the course.", 50, 1, 120, 3, 99, 2); + +INSERT INTO `program_assessments` + (`program_id`, `assessment_id`, `available_after`, `due_date`) +VALUES + (2, 1, "2033-02-06", "2033-06-10"), + (2, 2, "2023-02-06", "2033-06-24"), + (2, 3, "2023-02-20", "2033-06-30"); + +INSERT INTO `assessment_questions` + (`assessment_id`, `title`, `description`, `question_type_id`, `correct_answer_id`, `max_score`, `sort_order`) +VALUES + (1, "What is React?", NULL, 1, 4, 1, 1), + (1, "What is the purpose of JSX in React?", NULL, 1, 5, 1, 2), + (1, "What is the virtual DOM in React?", NULL, 1, 9, 1, 3), + (1, "What are React components?", NULL, 1, 14, 1, 4), + (1, "When lifting state up, you need to move the useState from a child component to a parent component.", NULL, 1, 18, 1, 5), + (1, "Which of the following options describe what a prop is in React", NULL, 1, 21, 1, 6), + (1, "If the state variable holds an array or a string value, once you pass that state via props from a parent to a child, you can then read the length property from the received prop in the child component", NULL, 1, 23, 1, 7), + (1, "Write a React JSX component that displays the text “Hello, World!” on the screen.", NULL, 2, NULL, 1, 8), + (1, "How does React differ from other JavaScript frameworks?", NULL, 2, NULL, 1, 9), + (1, "What are some of the benefits of using React?", NULL, 2, NULL, 1, 10), + (2, "A JOIN clause is used to combine rows from two or more tables, based on a related column between them. Which command is not a JOIN type in SQL?", NULL, 1, 30, 1, 1), + (2, "Which statement is wrong?", NULL, 1, 32, 2, 2), + (2, "How can we add a column to a table?", NULL, 1, 39, 1, 3), + (2, "Each table can contain more than one primary key.", NULL, 1, 41, 1, 4), + (3, "What is MySQL?", NULL, 1, 42, 1, 1), + (3, "INT and VARCHAR are some common data types in MySQL.", NULL, 1, 46, 1, 2), + (3, "Which command is used to create a new database in MySQL?", NULL, 1, 50, 1, 3), + (3, "COUNT, SUM, and INSERT INTO are some common MySQL aggregate functions.", NULL, 1, 53, 1, 4), + (3, "Which command is used to insert new data into a MySQL table?", NULL, 1, 57, 1, 5), + (3, "Which command is used to delete a table from a MySQL database?", NULL, 1, 59, 1, 6), + (3, "Which builtin MySQL function can be used to add every value from a column together in a query?", NULL, 1, 65, 1, 7), + (3, "Which command is used to retrieve data from a MySQL table?", NULL, 2, NULL, 9, 8), + (3, "Which function is used to count the number of rows in a MySQL table?", NULL, 2, NULL, 9, 9), + (3, "Which keyword is used to specify the condition for a MySQL query?", NULL, 2, NULL, 10, 10); + +INSERT INTO `assessment_answers` + (`question_id`, `title`, `description`, `sort_order`) +VALUES + (1, "A relational database management system", NULL, 1), + (1, "A database management system", NULL, 2), + (1, "A web server software", NULL, 3), + (1, "A front-end JavaScript library", NULL, 4), + (2, "To provide a syntax for writing HTML in JavaScript", NULL, 1), + (2, "To provide a syntax for writing JavaScript in HTML", NULL, 2), + (2, "To provide a syntax for writing SQL in JavaScript", NULL, 3), + (2, "To provide a syntax for writing CSS in JavaScript", NULL, 4), + (3, "A virtual representation of the actual HTML DOM", NULL, 1), + (3, "A physical representation of the actual HTML DOM", NULL, 2), + (3, "A database of all the components in a React application", NULL, 3), + (3, "A list of all the CSS styles used in a React application", NULL, 4), + (4, "An object-oriented programming language feature", NULL, 1), + (4, "Reusable pieces of UI", NULL, 2), + (4, "A way to define styles in React", NULL, 3), + (4, "A way to define database schemas in React", NULL, 4), + (5, "True", NULL, 1), + (5, "False", NULL, 2), + (6, "A built-in method for creating React components", NULL, 1), + (6, "A type of HTML tag used to define a custom element", NULL, 2), + (6, "A data object passed from a parent component to a child component", NULL, 3), + (6, "A tool that helps optimize the performance of a React app", NULL, 4), + (7, "True", NULL, 1), + (7, "False", NULL, 2), + (8, "const HelloWorld = () => { return

Hello, World!

; }; export default HelloWorld;", NULL, 1), + (9, "React differs from other JavaScript frameworks because it uses a component-based architecture, a virtual DOM, JSX syntax, unidirectional data flow, and is primarily focused on building UIs rather than providing a complete application framework. These features make it faster, more efficient, and more flexible than other frameworks.", NULL, 1), + (10, "Benefits of using React include its modular and reusable components, efficient updates with virtual DOM, JSX syntax, and active community.", NULL, 1), + (11, "Self Join", NULL, 1), + (11, "Full Join", NULL, 2), + (11, "Half Join", NULL, 3), + (11, "Inner Join", NULL, 4), + (12, "TypeScript is known as a dynamically-typed language", NULL, 1), + (12, "JavaScript is both a functional as well as object-oriented programming language", NULL, 2), + (12, "TypeScript takes a long time to compile the code", NULL, 3), + (12, "During compile time, the source code is translated to a byte code", NULL, 4), + (13, "CREATE TABLE", NULL, 1), + (13, "ADD DATA", NULL, 2), + (13, "INSERT DATA", NULL, 3), + (13, "ALTER TABLE", NULL, 4), + (14, "True", NULL, 1), + (14, "False", NULL, 2), + (15, "A relational database management system", NULL, 1), + (15, "A programming language", NULL, 2), + (15, "An operating system", NULL, 3), + (15, "A web server", NULL, 4), + (16, "True", NULL, 1), + (16, "False", NULL, 2), + (17, "CREATE TABLE", NULL, 1), + (17, "CREATE INDEX", NULL, 2), + (17, "CREATE DATABASE", NULL, 3), + (17, "CREATE SCHEMA", NULL, 4), + (18, "True", NULL, 1), + (18, "False", NULL, 2), + (19, "ADD DATA", NULL, 1), + (19, "INSERT DATA", NULL, 2), + (19, "INSERT ROW", NULL, 3), + (19, "INSERT INTO", NULL, 4), + (20, "DELETE TABLE", NULL, 1), + (20, "DROP TABLE", NULL, 2), + (20, "REMOVE TABLE", NULL, 3), + (20, "ERASE TABLE", NULL, 4), + (21, "MAX", NULL, 1), + (21, "TOGETHER", NULL, 2), + (21, "TOTAL", NULL, 3), + (21, "SUM", NULL, 4), + (21, "MIN", NULL, 5), + (21, "SUMTOTAL", NULL, 6), + (21, "TOTALSUM", NULL, 7), + (22, "The SELECT command is used to retrieve data from a MySQL table.", NULL, 1), + (23, "COUNT()", NULL, 1), + (24, "WHERE", NULL, 1); diff --git a/docker-compose.yml b/docker-compose.yml index 86ee101c..1eea50e4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ services: - SESSION_SECRET - WEBAPP_ORIGIN mysql: - command: --default-authentication-plugin=mysql_native_password + command: --skip-name-resolve environment: - MYSQL_DATABASE - MYSQL_PASSWORD @@ -29,9 +29,10 @@ services: - "3306:3306" healthcheck: test: mysql ${MYSQL_DATABASE} --user=${MYSQL_USER} --password='${MYSQL_PASSWORD}' --silent --execute "SELECT 1;" - interval: 10s - timeout: 10s - retries: 12 + interval: 3s + timeout: 3s + start_period: 60s + retries: 180 flyway: build: ./db/ command: migrate @@ -56,7 +57,7 @@ services: ports: - "80:80" redis: - image: redis:6.2-alpine + image: redis:7-alpine ports: - "6379:6379" webapp: diff --git a/nginx/Dockerfile b/nginx/Dockerfile index e005648b..b446ba55 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -1,3 +1,3 @@ -FROM nginx:1.23-alpine +FROM nginx:1.24-alpine ARG BUILD_ENV COPY ./${BUILD_ENV}/nginx.conf /etc/nginx/nginx.conf diff --git a/webapp/.nvmrc b/webapp/.nvmrc index ea438c37..d3e33db8 100644 --- a/webapp/.nvmrc +++ b/webapp/.nvmrc @@ -1 +1 @@ -v18.14 +v18.16 diff --git a/webapp/Dockerfile b/webapp/Dockerfile index b1a9b920..de7a1128 100644 --- a/webapp/Dockerfile +++ b/webapp/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18.14-alpine AS build +FROM node:18.16-alpine AS build ARG REACT_APP_API_ORIGIN ENV REACT_APP_API_ORIGIN $REACT_APP_API_ORIGIN ARG REACT_APP_ROLLBAR_ACCESS_TOKEN @@ -9,6 +9,6 @@ RUN yarn install --ignore-platform --frozen-lockfile --network-timeout 600000 COPY ./ ./ RUN yarn build -FROM nginx:1.23-alpine +FROM nginx:1.24-alpine COPY ./default.conf /etc/nginx/conf.d/default.conf COPY --from=build /app/build/ /usr/share/nginx/html/ diff --git a/webapp/package.json b/webapp/package.json index b85e148a..40621399 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -1,9 +1,9 @@ { "name": "@rhizone-lms/webapp", - "version": "0.0.0", + "version": "0.5.0", "private": true, "engines": { - "node": "^18.14.0", + "node": "^18.16.0", "yarn": "^1.22.19" }, "scripts": { @@ -17,32 +17,32 @@ "test": "TZ=UTC react-scripts test" }, "dependencies": { - "@babel/core": "^7.20.12", - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/icons-material": "^5.11.0", - "@mui/lab": "^5.0.0-alpha.119", - "@mui/material": "^5.11.8", - "@testing-library/dom": "^8.20.0", + "@babel/core": "^7.21.8", + "@emotion/react": "^11.11.0", + "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.11.16", + "@mui/lab": "^5.0.0-alpha.131", + "@mui/material": "^5.13.2", + "@testing-library/dom": "^9.3.0", "@testing-library/jest-dom": "^5.11.4", - "@testing-library/react": "^13.4.0", + "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", - "@types/jest": "^29.4.0", - "@types/luxon": "^3.2.0", - "@types/node": "^18.13.0", - "@types/react": "^18.0.27", - "@types/react-big-calendar": "^1.6.0", - "@types/react-dom": "^18.0.10", - "luxon": "^3.2.1", - "prettier": "^2.8.4", + "@types/jest": "^29.5.1", + "@types/luxon": "^3.3.0", + "@types/node": "^20.2.3", + "@types/react": "^18.2.6", + "@types/react-big-calendar": "^1.6.3", + "@types/react-dom": "^18.2.4", + "luxon": "^3.3.0", + "prettier": "^2.8.8", "react": "^18.2.0", - "react-big-calendar": "^1.6.4", + "react-big-calendar": "^1.6.9", "react-dom": "^18.2.0", - "react-router-dom": "^6.8.1", + "react-router-dom": "^6.11.2", "react-scripts": "5.0.1", - "socket.io-client": "^4.6.0", - "typescript": "^4.9.5", - "web-vitals": "^3.1.1" + "socket.io-client": "^4.6.1", + "typescript": "^5.0.4", + "web-vitals": "^3.3.1" }, "eslintConfig": { "extends": [ diff --git a/webapp/src/components/App.tsx b/webapp/src/components/App.tsx index a2e90d7e..501fbe39 100644 --- a/webapp/src/components/App.tsx +++ b/webapp/src/components/App.tsx @@ -11,6 +11,9 @@ import ReflectionsPage from './ReflectionsPage'; import RequireAuth from './RequireAuth'; import SessionContext from './SessionContext'; import ProgramsPage from './ProgramsPage'; +import AssessmentsListPage from './AssessmentsListPage'; +import AssessmentsDetailPage from './AssessmentDetailPage'; +import AssessmentSubmissionsListPage from './AssessmentSubmissionsListPage'; const App = () => { const { isAuthenticated } = useContext(SessionContext); @@ -59,6 +62,30 @@ const App = () => { } /> + + + + } + /> + + + + } + /> + + + + } + />