diff --git a/api/__mocks__/exam-environment-exam.ts b/api/__mocks__/exam-environment-exam.ts index 3f924469260..c6ec6a488bd 100644 --- a/api/__mocks__/exam-environment-exam.ts +++ b/api/__mocks__/exam-environment-exam.ts @@ -16,8 +16,9 @@ export const oid = () => new ObjectId().toString(); export const examId = oid(); -export const config: ExamEnvironmentConfig = { +export const config = { totalTimeInMS: 2 * 60 * 60 * 1000, + totalTimeInS: 2 * 60 * 60, tags: [], name: 'Test Exam', note: 'Some exam note...', @@ -45,8 +46,9 @@ export const config: ExamEnvironmentConfig = { numberOfIncorrectAnswers: 1 } ], - retakeTimeInMS: 24 * 60 * 60 * 1000 -}; + retakeTimeInMS: 24 * 60 * 60 * 1000, + retakeTimeInS: 24 * 60 * 60 +} satisfies ExamEnvironmentConfig; export const questionSets: ExamEnvironmentQuestionSet[] = [ { @@ -247,7 +249,7 @@ export const generatedExam: ExamEnvironmentGeneratedExam = { ] } ], - version: 1 + version: 2 }; export const examAttempt: ExamEnvironmentExamAttempt = { @@ -261,7 +263,8 @@ export const examAttempt: ExamEnvironmentExamAttempt = { { id: generatedExam.questionSets[0]!.questions[0]!.id, answers: [generatedExam.questionSets[0]!.questions[0]!.answers[0]!], - submissionTimeInMS: Date.now() + submissionTimeInMS: Date.now(), + submissionTime: new Date() } ] }, @@ -271,7 +274,8 @@ export const examAttempt: ExamEnvironmentExamAttempt = { { id: generatedExam.questionSets[1]!.questions[0]!.id, answers: [generatedExam.questionSets[1]!.questions[0]!.answers[1]!], - submissionTimeInMS: Date.now() + submissionTimeInMS: Date.now(), + submissionTime: new Date() } ] }, @@ -281,22 +285,25 @@ export const examAttempt: ExamEnvironmentExamAttempt = { { id: generatedExam.questionSets[2]!.questions[0]!.id, answers: [generatedExam.questionSets[2]!.questions[0]!.answers[1]!], - submissionTimeInMS: Date.now() + submissionTimeInMS: Date.now(), + submissionTime: new Date() }, { id: generatedExam.questionSets[2]!.questions[1]!.id, answers: [generatedExam.questionSets[2]!.questions[1]!.answers[0]!], - submissionTimeInMS: Date.now() + submissionTimeInMS: Date.now(), + submissionTime: new Date() } ] } ], startTimeInMS: Date.now(), + startTime: new Date(), userId: defaultUserId, - version: 1 + version: 2 }; -export const examAttemptSansSubmissionTimeInMS: Static< +export const examAttemptSansSubmissionTime: Static< typeof examEnvironmentPostExamAttempt.body >['attempt'] = { examId, @@ -335,20 +342,21 @@ export const examAttemptSansSubmissionTimeInMS: Static< ] }; -export const exam: ExamEnvironmentExam = { +export const exam = { id: examId, config, questionSets, prerequisites: ['67112fe1c994faa2c26d0b1d'], deprecated: false, - version: 1 -}; + version: 2 +} satisfies ExamEnvironmentExam; export const examEnvironmentChallenge: ExamEnvironmentChallenge = { id: oid(), examId, // Id of the certified full stack developer exam challenge page - challengeId: '645147516c245de4d11eb7ba' + challengeId: '645147516c245de4d11eb7ba', + version: 1 }; export async function seedEnvExam() { diff --git a/api/package.json b/api/package.json index 60dcab7afa2..51c0ec87b9e 100644 --- a/api/package.json +++ b/api/package.json @@ -13,7 +13,7 @@ "@fastify/swagger-ui": "5.2.0", "@fastify/type-provider-typebox": "5.1.0", "@growthbook/growthbook": "1.3.1", - "@prisma/client": "5.5.2", + "@prisma/client": "6.16.2", "@sentry/node": "9.1.0", "@sinclair/typebox": "^0.34.33", "@types/pino": "^7.0.5", @@ -51,7 +51,7 @@ "dotenv-cli": "7.3.0", "jsdom": "^26.1.0", "msw": "^2.7.0", - "prisma": "5.5.2", + "prisma": "6.16.2", "supertest": "6.3.3", "tsx": "4.19.1", "vitest": "^3.2.4" diff --git a/api/prisma.config.ts b/api/prisma.config.ts new file mode 100644 index 00000000000..e6fbe2e51db --- /dev/null +++ b/api/prisma.config.ts @@ -0,0 +1,5 @@ +import type { PrismaConfig } from 'prisma'; + +export default { + schema: 'prisma' +} satisfies PrismaConfig; diff --git a/api/prisma/exam-creator.prisma b/api/prisma/exam-creator.prisma new file mode 100644 index 00000000000..52eb3e58b44 --- /dev/null +++ b/api/prisma/exam-creator.prisma @@ -0,0 +1,62 @@ +/// A copy of `ExamEnvironmentExam` used as a staging collection for updates to the curriculum. +/// +/// This collection schema must be kept in sync with `ExamEnvironmentExam`. +model ExamCreatorExam { + /// Globally unique exam id + id String @id @default(auto()) @map("_id") @db.ObjectId + /// All questions for a given exam + questionSets ExamEnvironmentQuestionSet[] + /// Configuration for exam metadata + config ExamEnvironmentConfig + /// ObjectIds for required challenges/blocks to take the exam + prerequisites String[] @db.ObjectId + /// If `deprecated`, the exam should no longer be considered for users + deprecated Boolean + /// Version of the record + /// The default must be incremented by 1, if anything in the schema changes + version Int @default(2) + +} + +/// Exam Creator application collection to store authZ users. +/// +/// Currently, this is manually created in order to grant access to the application. +model ExamCreatorUser { + id String @id @default(auto()) @map("_id") @db.ObjectId + email String + /// Unique id from GitHub for an account. + /// + /// Currently, this is unused. Consider removing. + github_id Int? + name String + picture String? + /// TODO: After migration, remove optionality + settings ExamCreatorUserSettings? + version Int @default(1) + + + ExamCreatorSession ExamCreatorSession[] +} + +type ExamCreatorUserSettings { + databaseEnvironment ExamCreatorDatabaseEnvironment +} + +enum ExamCreatorDatabaseEnvironment { + Production + Staging +} + +/// Exam Creator application collection to store auth sessions. +model ExamCreatorSession { + id String @id @default(auto()) @map("_id") @db.ObjectId + user_id String @db.ObjectId + session_id String + /// Expiration date for record. + expires_at DateTime + + version Int @default(1) + + + ExamCreatorUser ExamCreatorUser @relation(fields: [user_id], references: [id]) +} diff --git a/api/prisma/exam-environment.prisma b/api/prisma/exam-environment.prisma new file mode 100644 index 00000000000..7419b14e50d --- /dev/null +++ b/api/prisma/exam-environment.prisma @@ -0,0 +1,259 @@ +/// An exam for the Exam Environment App as designed by the examiners +model ExamEnvironmentExam { + /// Globally unique exam id + id String @id @default(auto()) @map("_id") @db.ObjectId + /// All questions for a given exam + questionSets ExamEnvironmentQuestionSet[] + /// Configuration for exam metadata + config ExamEnvironmentConfig + /// ObjectIds for required challenges/blocks to take the exam + prerequisites String[] @db.ObjectId + /// If `deprecated`, the exam should no longer be considered for users + deprecated Boolean + /// Version of the record + /// The default must be incremented by 1, if anything in the schema changes + version Int @default(2) + + + // Relations + generatedExams ExamEnvironmentGeneratedExam[] + examAttempts ExamEnvironmentExamAttempt[] + ExamEnvironmentChallenge ExamEnvironmentChallenge[] +} + +/// A grouping of one or more questions of a given type +type ExamEnvironmentQuestionSet { + /// Unique question type id + id String @db.ObjectId + type ExamEnvironmentQuestionType + /// Content related to all questions in set + context String? + questions ExamEnvironmentMultipleChoiceQuestion[] +} + +/// A multiple choice question for the Exam Environment App +type ExamEnvironmentMultipleChoiceQuestion { + /// Unique question id + id String @db.ObjectId + /// Main question paragraph + text String + /// Zero or more tags given to categorize a question + tags String[] + /// Optional audio for a question + audio ExamEnvironmentAudio? + /// Available possible answers for an exam + answers ExamEnvironmentAnswer[] + /// TODO Possible "deprecated_time" to remove after all exams could possibly have been taken + deprecated Boolean +} + +/// Audio for an Exam Environment App multiple choice question +type ExamEnvironmentAudio { + /// Optional text for audio + captions String? + /// URL to audio file + /// + /// Expected in the format: `#t=,` + /// Where `start_time_in_seconds` and `end_time_in_seconds` are optional floats. + url String +} + +/// Type of question for the Exam Environment App +enum ExamEnvironmentQuestionType { + /// Single question with one or more answers + MultipleChoice + /// Mass text + Dialogue +} + +/// Answer for an Exam Environment App multiple choice question +type ExamEnvironmentAnswer { + /// Unique answer id + id String @db.ObjectId + /// Whether the answer is correct + isCorrect Boolean + /// Answer paragraph + text String +} + +/// Configuration for an exam in the Exam Environment App +type ExamEnvironmentConfig { + /// Human-readable exam name + name String + /// Notes given about exam + note String + /// Category configuration for question selection + tags ExamEnvironmentTagConfig[] + /// Deprecated: use `totalTimeInS` instead + totalTimeInMS Int + /// Total time allocated for exam in seconds + totalTimeInS Int? + /// Configuration for sets of questions + questionSets ExamEnvironmentQuestionSetConfig[] + /// Deprecated: use `retakeTimeInS` instead + retakeTimeInMS Int + /// Duration after exam completion before a retake is allowed in seconds + retakeTimeInS Int? + /// Passing percent for the exam + passingPercent Float +} + +/// Configuration for a set of questions in the Exam Environment App +type ExamEnvironmentQuestionSetConfig { + type ExamEnvironmentQuestionType + /// Number of this grouping of questions per exam + numberOfSet Int + /// Number of multiple choice questions per grouping matching this set config + numberOfQuestions Int + /// Number of correct answers given per multiple choice question + numberOfCorrectAnswers Int + /// Number of incorrect answers given per multiple choice question + numberOfIncorrectAnswers Int +} + +/// Configuration for tags in the Exam Environment App +/// +/// This configures the number of questions that should resolve to a given tag set criteria. +type ExamEnvironmentTagConfig { + /// Group of multiple choice question tags + group String[] + /// Number of multiple choice questions per exam that should meet the group criteria + numberOfQuestions Int +} + +/// An attempt at an exam in the Exam Environment App +model ExamEnvironmentExamAttempt { + id String @id @default(auto()) @map("_id") @db.ObjectId + /// Foriegn key to user + userId String @db.ObjectId + /// Foreign key to exam + examId String @db.ObjectId + /// Foreign key to generated exam id + generatedExamId String @db.ObjectId + + questionSets ExamEnvironmentQuestionSetAttempt[] + /// Deprecated: Use `startTime` instead + startTimeInMS Int + /// Time exam was started + startTime DateTime? + /// Version of the record + /// The default must be incremented by 1, if anything in the schema changes + version Int @default(2) + + + // Relations + user user @relation(fields: [userId], references: [id], onDelete: Cascade) + exam ExamEnvironmentExam @relation(fields: [examId], references: [id], onDelete: Cascade) + generatedExam ExamEnvironmentGeneratedExam @relation(fields: [generatedExamId], references: [id]) + ExamEnvironmentExamModeration ExamEnvironmentExamModeration[] +} + +type ExamEnvironmentQuestionSetAttempt { + id String @db.ObjectId + questions ExamEnvironmentMultipleChoiceQuestionAttempt[] +} + +type ExamEnvironmentMultipleChoiceQuestionAttempt { + /// Foreign key to question + id String @db.ObjectId + /// An array of foreign keys to answers + answers String[] @db.ObjectId + /// Deprecated: Use `submissionTime` instead + submissionTimeInMS Int + /// Time answers to question were submitted + /// + /// If the question is later revisited, this field is updated + submissionTime DateTime? +} + +/// A generated exam for the Exam Environment App +/// +/// This is the user-facing information for an exam. +model ExamEnvironmentGeneratedExam { + id String @id @default(auto()) @map("_id") @db.ObjectId + /// Foreign key to exam + examId String @db.ObjectId + questionSets ExamEnvironmentGeneratedQuestionSet[] + /// If `deprecated`, the generation should not longer be considered for users + deprecated Boolean + /// Version of the record + /// The default must be incremented by 1, if anything in the schema changes + version Int @default(1) + + + // Relations + exam ExamEnvironmentExam @relation(fields: [examId], references: [id], onDelete: Cascade) + EnvExamAttempt ExamEnvironmentExamAttempt[] +} + +type ExamEnvironmentGeneratedQuestionSet { + id String @db.ObjectId + questions ExamEnvironmentGeneratedMultipleChoiceQuestion[] +} + +type ExamEnvironmentGeneratedMultipleChoiceQuestion { + /// Foreign key to question id + id String @db.ObjectId + /// Each item is a foreign key to an answer + answers String[] @db.ObjectId +} + +/// A map between challenge ids and exam ids +/// +/// This is expected to be used for relating challenge pages AND/OR certifications to exams +model ExamEnvironmentChallenge { + id String @id @default(auto()) @map("_id") @db.ObjectId + examId String @db.ObjectId + challengeId String @db.ObjectId + + version Int @default(1) + + + exam ExamEnvironmentExam @relation(fields: [examId], references: [id], onDelete: Cascade) +} + +model ExamEnvironmentAuthorizationToken { + /// An ObjectId is used to provide access to the created timestamp + id String @id @default(auto()) @map("_id") @db.ObjectId + /// Used to set an `expireAt` index to delete documents + expireAt DateTime @db.Date + userId String @unique @db.ObjectId + version Int @default(1) + + + // Relations + user user @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model ExamEnvironmentExamModeration { + id String @id @default(auto()) @map("_id") @db.ObjectId + /// Whether or not the item is approved + status ExamEnvironmentExamModerationStatus + /// Foreign key to exam attempt + examAttemptId String @unique @db.ObjectId + /// Optional feedback/note about the moderation decision + feedback String? + /// Date the exam attempt was moderated + moderationDate DateTime? + /// Foreign key to moderator. This is `null` until the item is moderated. + moderatorId String? @db.ObjectId + + /// Date the exam attempt was added to the moderation queue + submissionDate DateTime @default(now()) @db.Date + /// Version of the record + /// The default must be incremented by 1, if anything in the schema changes + version Int @default(1) + + + // Relations + examAttempt ExamEnvironmentExamAttempt @relation(fields: [examAttemptId], references: [id], onDelete: Cascade) +} + +enum ExamEnvironmentExamModerationStatus { + /// Attempt is determined to be valid + Approved + /// Attempt is determined to be invalid + Denied + /// Attempt has yet to be moderated + Pending +} diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 0e765ffdac4..681d80f0d80 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -165,229 +165,8 @@ model user { isClassroomAccount Boolean? // Undefined // Relations - examAttempts ExamEnvironmentExamAttempt[] - examEnvironmentAuthorizationToken ExamEnvironmentAuthorizationToken? -} - -// ----------------------------------- - -/// An exam for the Exam Environment App as designed by the examiners -model ExamEnvironmentExam { - /// Globally unique exam id - id String @id @default(auto()) @map("_id") @db.ObjectId - /// All questions for a given exam - questionSets ExamEnvironmentQuestionSet[] - /// Configuration for exam metadata - config ExamEnvironmentConfig - /// ObjectIds for required challenges/blocks to take the exam - prerequisites String[] @db.ObjectId - /// If `deprecated`, the exam should no longer be considered for users - deprecated Boolean - /// Version of the record - /// The default must be incremented by 1, if anything in the schema changes - version Int @default(1) - - // Relations - generatedExams ExamEnvironmentGeneratedExam[] - examAttempts ExamEnvironmentExamAttempt[] - ExamEnvironmentChallenge ExamEnvironmentChallenge[] -} - -/// A copy of `ExamEnvironmentExam` used as a staging collection for updates to the curriculum. -/// -/// This collection schema must be kept in sync with `ExamEnvironmentExam`. -model ExamCreatorExam { - /// Globally unique exam id - id String @id @default(auto()) @map("_id") @db.ObjectId - /// All questions for a given exam - questionSets ExamEnvironmentQuestionSet[] - /// Configuration for exam metadata - config ExamEnvironmentConfig - /// ObjectIds for required challenges/blocks to take the exam - prerequisites String[] @db.ObjectId - /// If `deprecated`, the exam should no longer be considered for users - deprecated Boolean - /// Version of the record - /// The default must be incremented by 1, if anything in the schema changes - version Int @default(1) -} - -/// A grouping of one or more questions of a given type -type ExamEnvironmentQuestionSet { - /// Unique question type id - id String @db.ObjectId - type ExamEnvironmentQuestionType - /// Content related to all questions in set - context String? - questions ExamEnvironmentMultipleChoiceQuestion[] -} - -/// A multiple choice question for the Exam Environment App -type ExamEnvironmentMultipleChoiceQuestion { - /// Unique question id - id String @db.ObjectId - /// Main question paragraph - text String - /// Zero or more tags given to categorize a question - tags String[] - /// Optional audio for a question - audio ExamEnvironmentAudio? - /// Available possible answers for an exam - answers ExamEnvironmentAnswer[] - /// TODO Possible "deprecated_time" to remove after all exams could possibly have been taken - deprecated Boolean -} - -/// Audio for an Exam Environment App multiple choice question -type ExamEnvironmentAudio { - /// Optional text for audio - captions String? - /// URL to audio file - /// - /// Expected in the format: `#t=,` - /// Where `start_time_in_seconds` and `end_time_in_seconds` are optional floats. - url String -} - -/// Type of question for the Exam Environment App -enum ExamEnvironmentQuestionType { - /// Single question with one or more answers - MultipleChoice - /// Mass text - Dialogue -} - -/// Answer for an Exam Environment App multiple choice question -type ExamEnvironmentAnswer { - /// Unique answer id - id String @db.ObjectId - /// Whether the answer is correct - isCorrect Boolean - /// Answer paragraph - text String -} - -/// Configuration for an exam in the Exam Environment App -type ExamEnvironmentConfig { - /// Human-readable exam name - name String - /// Notes given about exam - note String - /// Category configuration for question selection - tags ExamEnvironmentTagConfig[] - /// Total time allocated for exam in milliseconds - totalTimeInMS Int - /// Configuration for sets of questions - questionSets ExamEnvironmentQuestionSetConfig[] - /// Duration after exam completion before a retake is allowed in milliseconds - retakeTimeInMS Int - /// Passing percent for the exam - passingPercent Float -} - -/// Configuration for a set of questions in the Exam Environment App -type ExamEnvironmentQuestionSetConfig { - type ExamEnvironmentQuestionType - /// Number of this grouping of questions per exam - numberOfSet Int - /// Number of multiple choice questions per grouping matching this set config - numberOfQuestions Int - /// Number of correct answers given per multiple choice question - numberOfCorrectAnswers Int - /// Number of incorrect answers given per multiple choice question - numberOfIncorrectAnswers Int -} - -/// Configuration for tags in the Exam Environment App -/// -/// This configures the number of questions that should resolve to a given tag set criteria. -type ExamEnvironmentTagConfig { - /// Group of multiple choice question tags - group String[] - /// Number of multiple choice questions per exam that should meet the group criteria - numberOfQuestions Int -} - -/// An attempt at an exam in the Exam Environment App -model ExamEnvironmentExamAttempt { - id String @id @default(auto()) @map("_id") @db.ObjectId - /// Foriegn key to user - userId String @db.ObjectId - /// Foreign key to exam - examId String @db.ObjectId - /// Foreign key to generated exam id - generatedExamId String @db.ObjectId - - questionSets ExamEnvironmentQuestionSetAttempt[] - /// Time exam was started as milliseconds since epoch - startTimeInMS Int - /// Version of the record - /// The default must be incremented by 1, if anything in the schema changes - version Int @default(1) - - // Relations - user user @relation(fields: [userId], references: [id], onDelete: Cascade) - exam ExamEnvironmentExam @relation(fields: [examId], references: [id], onDelete: Cascade) - generatedExam ExamEnvironmentGeneratedExam @relation(fields: [generatedExamId], references: [id]) - ExamEnvironmentExamModeration ExamEnvironmentExamModeration[] -} - -type ExamEnvironmentQuestionSetAttempt { - id String @db.ObjectId - questions ExamEnvironmentMultipleChoiceQuestionAttempt[] -} - -type ExamEnvironmentMultipleChoiceQuestionAttempt { - /// Foreign key to question - id String @db.ObjectId - /// An array of foreign keys to answers - answers String[] @db.ObjectId - /// Time answers to question were submitted as milliseconds since epoch - /// - /// If the question is later revisited, this field is updated - submissionTimeInMS Int -} - -/// A generated exam for the Exam Environment App -/// -/// This is the user-facing information for an exam. -model ExamEnvironmentGeneratedExam { - id String @id @default(auto()) @map("_id") @db.ObjectId - /// Foreign key to exam - examId String @db.ObjectId - questionSets ExamEnvironmentGeneratedQuestionSet[] - /// If `deprecated`, the generation should not longer be considered for users - deprecated Boolean - /// Version of the record - /// The default must be incremented by 1, if anything in the schema changes - version Int @default(1) - - // Relations - exam ExamEnvironmentExam @relation(fields: [examId], references: [id], onDelete: Cascade) - EnvExamAttempt ExamEnvironmentExamAttempt[] -} - -type ExamEnvironmentGeneratedQuestionSet { - id String @db.ObjectId - questions ExamEnvironmentGeneratedMultipleChoiceQuestion[] -} - -type ExamEnvironmentGeneratedMultipleChoiceQuestion { - /// Foreign key to question id - id String @db.ObjectId - /// Each item is a foreign key to an answer - answers String[] @db.ObjectId -} - -/// A map between challenge ids and exam ids -/// -/// This is expected to be used for relating challenge pages AND/OR certifications to exams -model ExamEnvironmentChallenge { - id String @id @default(auto()) @map("_id") @db.ObjectId - examId String @db.ObjectId - challengeId String @db.ObjectId - - exam ExamEnvironmentExam @relation(fields: [examId], references: [id], onDelete: Cascade) + examAttempts ExamEnvironmentExamAttempt[] + examEnvironmentAuthorizationToken ExamEnvironmentAuthorizationToken? } // ----------------------------------- @@ -433,17 +212,6 @@ model UserToken { @@index([userId], map: "userId_1") } -model ExamEnvironmentAuthorizationToken { - /// An ObjectId is used to provide access to the created timestamp - id String @id @default(auto()) @map("_id") @db.ObjectId - /// Used to set an `expireAt` index to delete documents - expireAt DateTime @db.Date - userId String @unique @db.ObjectId - - // Relations - user user @relation(fields: [userId], references: [id], onDelete: Cascade) -} - model sessions { id String @id @map("_id") expires DateTime @db.Date @@ -556,64 +324,3 @@ type DailyCodingChallengeApiLanguageChallengeFiles { contents String fileKey String } - -// ---------------------- - -model ExamEnvironmentExamModeration { - id String @id @default(auto()) @map("_id") @db.ObjectId - /// Whether or not the item is approved - status ExamEnvironmentExamModerationStatus - /// Foreign key to exam attempt - examAttemptId String @unique @db.ObjectId - /// Optional feedback/note about the moderation decision - feedback String? - /// Date the exam attempt was moderated - moderationDate DateTime? - /// Foreign key to moderator. This is `null` until the item is moderated. - moderatorId String? @db.ObjectId - - /// Date the exam attempt was added to the moderation queue - submissionDate DateTime @default(now()) @db.Date - /// Version of the record - /// The default must be incremented by 1, if anything in the schema changes - version Int @default(1) - - // Relations - examAttempt ExamEnvironmentExamAttempt @relation(fields: [examAttemptId], references: [id], onDelete: Cascade) -} - -enum ExamEnvironmentExamModerationStatus { - /// Attempt is determined to be valid - Approved - /// Attempt is determined to be invalid - Denied - /// Attempt has yet to be moderated - Pending -} - -/// Exam Creator application collection to store authZ users. -/// -/// Currently, this is manually created in order to grant access to the application. -model ExamCreatorUser { - id String @id @default(auto()) @map("_id") @db.ObjectId - email String - /// Unique id from GitHub for an account. - /// - /// Currently, this is unused. Consider removing. - github_id Int? - name String - picture String? - - ExamCreatorSession ExamCreatorSession[] -} - -/// Exam Creator application collection to store auth sessions. -model ExamCreatorSession { - id String @id @default(auto()) @map("_id") @db.ObjectId - user_id String @db.ObjectId - session_id String - /// Expiration date for record. - expires_at DateTime - - ExamCreatorUser ExamCreatorUser @relation(fields: [user_id], references: [id]) -} diff --git a/api/src/exam-environment/routes/exam-environment.test.ts b/api/src/exam-environment/routes/exam-environment.test.ts index 9c73f3e22f5..190701edb69 100644 --- a/api/src/exam-environment/routes/exam-environment.test.ts +++ b/api/src/exam-environment/routes/exam-environment.test.ts @@ -16,6 +16,7 @@ import { createSuperRequest, defaultUserId, devLogin, + serializeDates, setupServer } from '../../../vitest.utils.js'; import { @@ -102,6 +103,7 @@ describe('/exam-environment/', () => { examId, generatedExamId: mock.oid(), startTimeInMS: Date.now(), + startTime: new Date(), userId: defaultUserId } }); @@ -133,6 +135,7 @@ describe('/exam-environment/', () => { examId: mock.examId, generatedExamId: mock.oid(), startTimeInMS: Date.now() - (1000 * 60 * 60 * 2 + 1000), + startTime: new Date(Date.now() - (1000 * 60 * 60 * 2 + 1000)), userId: defaultUserId } }); @@ -164,6 +167,7 @@ describe('/exam-environment/', () => { examId: mock.examId, generatedExamId: mock.oid(), startTimeInMS: Date.now(), + startTime: new Date(), userId: defaultUserId } }); @@ -235,12 +239,13 @@ describe('/exam-environment/', () => { examId: mock.examId, generatedExamId: mock.generatedExam.id, startTimeInMS: Date.now(), + startTime: new Date(), questionSets: [] } }); const body: Static = { - attempt: mock.examAttemptSansSubmissionTimeInMS + attempt: mock.examAttemptSansSubmissionTime }; const res = await superPost('/exam-environment/exam/attempt') @@ -330,10 +335,13 @@ describe('/exam-environment/', () => { }); it('should return an error if the exam has been attempted too recently to retake', async () => { + const examTotalTimeInMS = mock.exam.config.totalTimeInS * 1000; + const recentExamAttempt = { ...mock.examAttempt, // Set start time such that exam has just expired - startTimeInMS: Date.now() - mock.exam.config.totalTimeInMS + startTimeInMS: Date.now() - examTotalTimeInMS, + startTime: new Date(Date.now() - examTotalTimeInMS) }; await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({ data: recentExamAttempt @@ -357,6 +365,8 @@ describe('/exam-environment/', () => { } }); + const examRetakeTimeInMS = mock.exam.config.retakeTimeInS * 1000; + await fastifyTestInstance.prisma.examEnvironmentExamAttempt.update({ where: { id: recentExamAttempt.id @@ -364,9 +374,10 @@ describe('/exam-environment/', () => { data: { // Set start time such that exam has expired, but retake time -1s has passed startTimeInMS: - Date.now() - - (mock.exam.config.totalTimeInMS + - (mock.exam.config.retakeTimeInMS - 1000)) + Date.now() - (examTotalTimeInMS + (examRetakeTimeInMS - 1000)), + startTime: new Date( + Date.now() - (examTotalTimeInMS + (examRetakeTimeInMS - 1000)) + ) } }); @@ -393,9 +404,14 @@ describe('/exam-environment/', () => { it('should use a new exam attempt if all previous attempts were started > 24 hours ago', async () => { const recentExamAttempt = structuredClone(mock.examAttempt); // Set start time such that exam has expired, but 24 hours + 1s has passed + const examTotalTimeInMS = mock.exam.config.totalTimeInS * 1000; + recentExamAttempt.startTimeInMS = - Date.now() - - (mock.exam.config.totalTimeInMS + (24 * 60 * 60 * 1000 + 1000)); + Date.now() - examTotalTimeInMS + (24 * 60 * 60 * 1000 + 1000); + recentExamAttempt.startTime = new Date( + Date.now() - (examTotalTimeInMS + (24 * 60 * 60 * 1000 + 1000)) + ); + await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({ data: recentExamAttempt }); @@ -450,7 +466,7 @@ describe('/exam-environment/', () => { expect(res).toMatchObject({ status: 200, body: { - examAttempt: latestAttempt + examAttempt: serializeDates(latestAttempt) } }); }); @@ -458,12 +474,20 @@ describe('/exam-environment/', () => { it('should return an error if the database has insufficient generated exams', async () => { // Add completed attempt for generated exam const submittedAttempt = structuredClone(mock.examAttempt); + const examTotalTimeInMS = mock.exam.config.totalTimeInS * 1000; // Long-enough ago to be considered "submitted", and not trigger cooldown submittedAttempt.startTimeInMS = Date.now() - 24 * 60 * 60 * 1000 - - mock.exam.config.totalTimeInMS - + examTotalTimeInMS - 1 * 60 * 60 * 1000; + submittedAttempt.startTime = new Date( + Date.now() - + 24 * 60 * 60 * 1000 - + examTotalTimeInMS - + 1 * 60 * 60 * 1000 + ); + await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({ data: submittedAttempt }); @@ -523,6 +547,8 @@ describe('/exam-environment/', () => { generatedExamId: generatedExam!.id, questionSets: [], // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + startTime: expect.any(Date), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment startTimeInMS: expect.any(Number), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment version: expect.any(Number) @@ -594,10 +620,12 @@ describe('/exam-environment/', () => { const userExam = constructUserExam(generatedExam!, mock.exam); - expect(res.body).toMatchObject({ - examAttempt, - exam: userExam - }); + expect(res.body).toMatchObject( + serializeDates({ + examAttempt, + exam: userExam + }) + ); }); }); @@ -638,7 +666,9 @@ describe('/exam-environment/', () => { name: mock.exam.config.name, note: mock.exam.config.note, passingPercent: mock.exam.config.passingPercent, + totalTimeInS: mock.exam.config.totalTimeInS, totalTimeInMS: mock.exam.config.totalTimeInMS, + retakeTimeInS: mock.exam.config.retakeTimeInS, retakeTimeInMS: mock.exam.config.retakeTimeInMS }, id: mock.examId @@ -713,7 +743,9 @@ describe('/exam-environment/', () => { name: mock.exam.config.name, note: mock.exam.config.note, passingPercent: mock.exam.config.passingPercent, + totalTimeInS: mock.exam.config.totalTimeInS, totalTimeInMS: mock.exam.config.totalTimeInMS, + retakeTimeInS: mock.exam.config.retakeTimeInS, retakeTimeInMS: mock.exam.config.retakeTimeInMS }, id: mock.examId @@ -725,10 +757,13 @@ describe('/exam-environment/', () => { it("should indicate an exam's availability based on the last attempt's start time, and the exam retake time", async () => { // Create a recent exam attempt that's within the retake time + const examTotalTimeInMS = mock.exam.config.totalTimeInS * 1000; + const recentExamAttempt = { ...mock.examAttempt, userId: defaultUserId, - startTimeInMS: Date.now() - mock.exam.config.totalTimeInMS + startTimeInMS: Date.now() - examTotalTimeInMS, + startTime: new Date(Date.now() - examTotalTimeInMS) }; await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({ data: recentExamAttempt @@ -742,15 +777,17 @@ describe('/exam-environment/', () => { expect(res.body).toMatchObject([{ canTake: false }]); expect(res.status).toBe(200); + const examRetakeTimeInMS = mock.exam.config.retakeTimeInS * 1000; + // Update the attempt to be outside the retake time await fastifyTestInstance.prisma.examEnvironmentExamAttempt.update({ where: { id: recentExamAttempt.id }, data: { startTimeInMS: - Date.now() - - (mock.exam.config.totalTimeInMS + - mock.exam.config.retakeTimeInMS + - 1000) + Date.now() - (examTotalTimeInMS + examRetakeTimeInMS + 1000), + startTime: new Date( + Date.now() - (examTotalTimeInMS + examRetakeTimeInMS + 1000) + ) } }); @@ -766,14 +803,16 @@ describe('/exam-environment/', () => { it('should indicate an exam is unavailable if there are any pending moderation records for the exam attempts', async () => { // Create an exam attempt that's outside the retake time + const examTotalTimeInMS = mock.exam.config.totalTimeInS * 1000; + const examRetakeTimeInMS = mock.exam.config.retakeTimeInS * 1000; const examAttempt = { ...mock.examAttempt, userId: defaultUserId, startTimeInMS: - Date.now() - - (mock.exam.config.totalTimeInMS + - mock.exam.config.retakeTimeInMS + - 1000) + Date.now() - (examTotalTimeInMS + examRetakeTimeInMS + 1000), + startTime: new Date( + Date.now() - (examTotalTimeInMS + examRetakeTimeInMS + 1000) + ) }; const createdAttempt = await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({ @@ -868,11 +907,12 @@ describe('/exam-environment/', () => { id: attempt.id, examId: mock.exam.id, result: null, + startTime: attempt.startTime, startTimeInMS: attempt.startTimeInMS, questionSets: attempt.questionSets }; - expect(res.body).toEqual(examEnvironmentExamAttempt); + expect(res.body).toEqual(serializeDates(examEnvironmentExamAttempt)); expect(res.status).toBe(200); }); @@ -908,17 +948,21 @@ describe('/exam-environment/', () => { id: attempt.id, examId: mock.exam.id, result: null, + startTime: attempt.startTime, startTimeInMS: attempt.startTimeInMS, questionSets: attempt.questionSets }; - expect(res.body).toEqual(examEnvironmentExamAttempt); + expect(res.body).toEqual(serializeDates(examEnvironmentExamAttempt)); expect(res.status).toBe(200); }); it('should return the attempt with results, if the attempt has been moderated', async () => { const examAttempt = structuredClone(mock.examAttempt); - examAttempt.startTimeInMS = Date.now() - mock.exam.config.totalTimeInMS; + const examTotalTimeInMS = mock.exam.config.totalTimeInS * 1000; + + examAttempt.startTimeInMS = Date.now() - examTotalTimeInMS; + examAttempt.startTime = new Date(Date.now() - examTotalTimeInMS); const attempt = await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({ data: examAttempt @@ -945,11 +989,12 @@ describe('/exam-environment/', () => { score: 25, passingPercent: 80 }, + startTime: attempt.startTime, startTimeInMS: attempt.startTimeInMS, questionSets: attempt.questionSets }; - expect(res.body).toEqual(examEnvironmentExamAttempt); + expect(res.body).toEqual(serializeDates(examEnvironmentExamAttempt)); expect(res.status).toBe(200); }); }); @@ -1000,11 +1045,12 @@ describe('/exam-environment/', () => { id: attempt.id, examId: mock.exam.id, result: null, + startTime: attempt.startTime, startTimeInMS: attempt.startTimeInMS, questionSets: attempt.questionSets }; - expect(res.body).toEqual([examEnvironmentExamAttempt]); + expect(res.body).toEqual([serializeDates(examEnvironmentExamAttempt)]); expect(res.status).toBe(200); }); @@ -1030,17 +1076,21 @@ describe('/exam-environment/', () => { id: attempt.id, examId: mock.exam.id, result: null, + startTime: attempt.startTime, startTimeInMS: attempt.startTimeInMS, questionSets: attempt.questionSets }; - expect(res.body).toEqual([examEnvironmentExamAttempt]); + expect(res.body).toEqual([serializeDates(examEnvironmentExamAttempt)]); expect(res.status).toBe(200); }); it('should return the attempts with results, if they have been moderated', async () => { const examAttempt = structuredClone(mock.examAttempt); - examAttempt.startTimeInMS = Date.now() - mock.exam.config.totalTimeInMS; + const examTotalTimeInMS = mock.exam.config.totalTimeInS * 1000; + + examAttempt.startTimeInMS = Date.now() - examTotalTimeInMS; + examAttempt.startTime = new Date(Date.now() - examTotalTimeInMS); const attempt = await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({ data: examAttempt @@ -1065,11 +1115,12 @@ describe('/exam-environment/', () => { score: 25, passingPercent: 80 }, + startTime: attempt.startTime, startTimeInMS: attempt.startTimeInMS, questionSets: attempt.questionSets }; - expect(res.body).toEqual([examEnvironmentExamAttempt]); + expect(res.body).toEqual([serializeDates(examEnvironmentExamAttempt)]); expect(res.status).toBe(200); }); }); @@ -1115,12 +1166,14 @@ describe('/exam-environment/', () => { id: attempt.id, examId: mock.exam.id, result: null, + startTime: attempt.startTime, startTimeInMS: attempt.startTimeInMS, questionSets: attempt.questionSets, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment version: expect.any(Number) }; - expect(res.body).toEqual([examEnvironmentExamAttempt]); + + expect(res.body).toEqual([serializeDates(examEnvironmentExamAttempt)]); expect(res.status).toBe(200); }); }); diff --git a/api/src/exam-environment/routes/exam-environment.ts b/api/src/exam-environment/routes/exam-environment.ts index 929b14f8361..35ab0bb39e0 100644 --- a/api/src/exam-environment/routes/exam-environment.ts +++ b/api/src/exam-environment/routes/exam-environment.ts @@ -259,9 +259,13 @@ async function postExamGeneratedExamHandler( const examAttempts = maybeExamAttempts.data; const lastAttempt = examAttempts.length - ? examAttempts.reduce((latest, current) => - latest.startTimeInMS > current.startTimeInMS ? latest : current - ) + ? examAttempts.reduce((latest, current) => { + const latestStartTime = + latest.startTime?.getTime() ?? latest.startTimeInMS; + const currentStartTime = + current.startTime?.getTime() ?? current.startTimeInMS; + return latestStartTime > currentStartTime ? latest : current; + }) : null; if (lastAttempt) { @@ -300,11 +304,19 @@ async function postExamGeneratedExamHandler( ); } - const examExpirationTime = - lastAttempt.startTimeInMS + exam.config.totalTimeInMS; + const lastAttemptStartTime = + lastAttempt.startTime?.getTime() ?? lastAttempt.startTimeInMS; + const examTotalTimeInMS = exam.config.totalTimeInS + ? exam.config.totalTimeInS * 1000 + : exam.config.totalTimeInMS; + const examExpirationTime = lastAttemptStartTime + examTotalTimeInMS; + if (examExpirationTime < Date.now()) { + const examRetakeTimeInMS = exam.config.retakeTimeInS + ? exam.config.retakeTimeInS * 1000 + : exam.config.retakeTimeInMS; const retakeAllowed = - examExpirationTime + exam.config.retakeTimeInMS < Date.now(); + examExpirationTime + examRetakeTimeInMS < Date.now(); if (!retakeAllowed) { logger.warn( @@ -444,6 +456,7 @@ async function postExamGeneratedExamHandler( examId: exam.id, generatedExamId: generatedExam.id, startTimeInMS: Date.now(), + startTime: new Date(), questionSets: [] } }) @@ -540,9 +553,12 @@ async function postExamAttemptHandler( ); } - const latestAttempt = attempts.reduce((latest, current) => - latest.startTimeInMS > current.startTimeInMS ? latest : current - ); + const latestAttempt = attempts.reduce((latest, current) => { + const latestStartTime = latest.startTime?.getTime() ?? latest.startTimeInMS; + const currentStartTime = + current.startTime?.getTime() ?? current.startTimeInMS; + return latestStartTime > currentStartTime ? latest : current; + }); const maybeExam = await mapErr( this.prisma.examEnvironmentExam.findUnique({ @@ -573,8 +589,13 @@ async function postExamAttemptHandler( ); } + const latestAttemptStartTime = + latestAttempt.startTime?.getTime() ?? latestAttempt.startTimeInMS; + const examTotalTimeInMS = exam.config.totalTimeInS + ? exam.config.totalTimeInS * 1000 + : exam.config.totalTimeInMS; const isAttemptExpired = - latestAttempt.startTimeInMS + exam.config.totalTimeInMS < Date.now(); + latestAttemptStartTime + examTotalTimeInMS < Date.now(); if (isAttemptExpired) { logger.warn( @@ -715,7 +736,8 @@ async function getExams( select: { id: true, examId: true, - startTimeInMS: true + startTimeInMS: true, + startTime: true } }) ); @@ -740,7 +762,9 @@ async function getExams( name: exam.config.name, note: exam.config.note, totalTimeInMS: exam.config.totalTimeInMS, + totalTimeInS: exam.config.totalTimeInS, retakeTimeInMS: exam.config.retakeTimeInMS, + retakeTimeInS: exam.config.retakeTimeInS, passingPercent: exam.config.passingPercent }, canTake: false @@ -762,9 +786,13 @@ async function getExams( const attemptsForExam = attempts.filter(a => a.examId === exam.id); const lastAttempt = attemptsForExam.length - ? attemptsForExam.reduce((latest, current) => - latest.startTimeInMS > current.startTimeInMS ? latest : current - ) + ? attemptsForExam.reduce((latest, current) => { + const latestStartTime = + latest.startTime?.getTime() ?? latest.startTimeInMS; + const currentStartTime = + current.startTime?.getTime() ?? current.startTimeInMS; + return latestStartTime > currentStartTime ? latest : current; + }) : null; if (!lastAttempt) { @@ -774,10 +802,16 @@ async function getExams( continue; } + const lastAttemptStartTime = + lastAttempt.startTime?.getTime() ?? lastAttempt.startTimeInMS; + const examTotalTimeInMS = exam.config.totalTimeInS + ? exam.config.totalTimeInS * 1000 + : exam.config.totalTimeInMS; + const examRetakeTimeInMS = exam.config.retakeTimeInS + ? exam.config.retakeTimeInS * 1000 + : exam.config.retakeTimeInMS; const retakeDateInMS = - lastAttempt.startTimeInMS + - exam.config.totalTimeInMS + - exam.config.retakeTimeInMS; + lastAttemptStartTime + examTotalTimeInMS + examRetakeTimeInMS; const isRetakeTimePassed = Date.now() > retakeDateInMS; if (!isRetakeTimePassed) { diff --git a/api/src/exam-environment/schemas/exam-environment-exam-attempt.ts b/api/src/exam-environment/schemas/exam-environment-exam-attempt.ts index 5ad85a310e2..c2a07aa87c7 100644 --- a/api/src/exam-environment/schemas/exam-environment-exam-attempt.ts +++ b/api/src/exam-environment/schemas/exam-environment-exam-attempt.ts @@ -30,6 +30,7 @@ const examEnvAttempt = Type.Object({ id: Type.String(), examId: Type.String(), startTimeInMS: Type.Number(), + startTime: Type.String({ format: 'date-time' }), questionSets: Type.Array( Type.Object({ id: Type.String(), @@ -37,7 +38,8 @@ const examEnvAttempt = Type.Object({ Type.Object({ id: Type.String(), answers: Type.Array(Type.String()), - submissionTimeInMS: Type.Number() + submissionTimeInMS: Type.Number(), + submissionTime: Type.String({ format: 'date-time' }) }) ) }) diff --git a/api/src/exam-environment/schemas/exam-environment-exams.ts b/api/src/exam-environment/schemas/exam-environment-exams.ts index 5bf153bf6eb..ac2239eaf32 100644 --- a/api/src/exam-environment/schemas/exam-environment-exams.ts +++ b/api/src/exam-environment/schemas/exam-environment-exams.ts @@ -12,7 +12,9 @@ export const examEnvironmentExams = { name: Type.String(), note: Type.String(), totalTimeInMS: Type.Number(), + totalTimeInS: Type.Number(), retakeTimeInMS: Type.Number(), + retakeTimeInS: Type.Number(), passingPercent: Type.Number() }), canTake: Type.Boolean() diff --git a/api/src/exam-environment/utils/exam-environment.test.ts b/api/src/exam-environment/utils/exam-environment.test.ts index 88b6a037b29..11cf458e798 100644 --- a/api/src/exam-environment/utils/exam-environment.test.ts +++ b/api/src/exam-environment/utils/exam-environment.test.ts @@ -239,7 +239,7 @@ describe('Exam Environment mocked Math.random', () => { const allQuestions = databaseAttemptQuestionSets.flatMap( qs => qs.questions ); - expect(allQuestions.every(q => q.submissionTimeInMS)).toBe(true); + expect(allQuestions.every(q => q.submissionTime)).toBe(true); }); it('should not change the submission time of any questions that have not changed', () => { @@ -264,7 +264,7 @@ describe('Exam Environment mocked Math.random', () => { userAttemptToDatabaseAttemptQuestionSets(userAttempt, latestAttempt); const submissionTimes = databaseAttemptQuestionSets.flatMap(qs => - qs.questions.map(q => q.submissionTimeInMS) + qs.questions.map(q => q.submissionTime) ); const sameAttempt = userAttemptToDatabaseAttemptQuestionSets( @@ -273,7 +273,7 @@ describe('Exam Environment mocked Math.random', () => { ); const sameSubmissionTimes = sameAttempt.flatMap(qs => - qs.questions.map(q => q.submissionTimeInMS) + qs.questions.map(q => q.submissionTime) ); expect(submissionTimes).toEqual(sameSubmissionTimes); @@ -314,9 +314,9 @@ describe('Exam Environment mocked Math.random', () => { ); expect( - newAttemptQuestionSets[0]?.questions[0]?.submissionTimeInMS + newAttemptQuestionSets[0]?.questions[0]?.submissionTime ).not.toEqual( - databaseAttemptQuestionSets[0]?.questions[0]?.submissionTimeInMS + databaseAttemptQuestionSets[0]?.questions[0]?.submissionTime ); }); }); @@ -447,8 +447,10 @@ describe('Exam Environment Schema', () => { passingPercent: 0.0, questionSets: configQuestionSets, retakeTimeInMS: 0, + retakeTimeInS: 0, tags, - totalTimeInMS: 0 + totalTimeInMS: 0, + totalTimeInS: 0 }; const questions = [ @@ -522,11 +524,17 @@ describe('Exam Environment Schema', () => { { id: oid(), questions: [ - { answers: [oid()], id: oid(), submissionTimeInMS: 0 } + { + answers: [oid()], + id: oid(), + submissionTime: new Date(), + submissionTimeInMS: Date.now() + } ] } ], - startTimeInMS: 0, + startTimeInMS: Date.now(), + startTime: new Date(), userId: oid() } }); diff --git a/api/src/exam-environment/utils/exam-environment.ts b/api/src/exam-environment/utils/exam-environment.ts index 1e2772254c0..b72db4fab91 100644 --- a/api/src/exam-environment/utils/exam-environment.ts +++ b/api/src/exam-environment/utils/exam-environment.ts @@ -112,9 +112,11 @@ export function constructUserExam( const config = { totalTimeInMS: exam.config.totalTimeInMS, + totalTimeInS: exam.config.totalTimeInS, name: exam.config.name, note: exam.config.note, retakeTimeInMS: exam.config.retakeTimeInMS, + retakeTimeInS: exam.config.retakeTimeInS, passingPercent: exam.config.passingPercent }; @@ -241,7 +243,11 @@ export function userAttemptToDatabaseAttemptQuestionSets( databaseAttemptQuestionSets.push({ ...questionSet, questions: questionSet.questions.map(q => { - return { ...q, submissionTimeInMS: Date.now() }; + return { + ...q, + submissionTime: new Date(), + submissionTimeInMS: Date.now() + }; }) }); } else { @@ -254,14 +260,22 @@ export function userAttemptToDatabaseAttemptQuestionSets( // If no latest question, add submission time if (!latestQuestion) { - return { ...q, submissionTimeInMS: Date.now() }; + return { + ...q, + submissionTime: new Date(), + submissionTimeInMS: Date.now() + }; } // If answers have changed, add submission time if ( JSON.stringify(q.answers) !== JSON.stringify(latestQuestion.answers) ) { - return { ...q, submissionTimeInMS: Date.now() }; + return { + ...q, + submissionTime: new Date(), + submissionTimeInMS: Date.now() + }; } return latestQuestion; @@ -810,8 +824,13 @@ export async function constructEnvExamAttempt( } // If attempt is still in progress, return without result + const attemptStartTimeInMS = + attempt.startTime?.getTime() ?? attempt.startTimeInMS; + const examTotalTimeInMS = exam.config.totalTimeInS + ? exam.config.totalTimeInS * 1000 + : exam.config.totalTimeInMS; const isAttemptExpired = - attempt.startTimeInMS + exam.config.totalTimeInMS < Date.now(); + attemptStartTimeInMS + examTotalTimeInMS < Date.now(); if (!isAttemptExpired) { return { examEnvironmentExamAttempt: { diff --git a/api/tools/docker-compose.yml b/api/tools/docker-compose.yml index 72bf0a4e991..070fcccc3a7 100644 --- a/api/tools/docker-compose.yml +++ b/api/tools/docker-compose.yml @@ -7,7 +7,7 @@ services: ports: - 27017:27017 volumes: - - db-data:/data + - db-data:/data/db healthcheck: test: ['CMD', 'mongosh', '--eval', "db.adminCommand('ping')"] interval: 2s @@ -31,3 +31,4 @@ services: volumes: db-data: + driver: local diff --git a/api/vitest.utils.test.ts b/api/vitest.utils.test.ts index 1bcccd43f89..e1fad63317d 100644 --- a/api/vitest.utils.test.ts +++ b/api/vitest.utils.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from 'vitest'; -import { getCsrfToken, getCookies } from './vitest.utils.js'; +import { getCsrfToken, getCookies, serializeDates } from './vitest.utils.js'; const fakeCookies = [ '_csrf=123; Path=/; HttpOnly; SameSite=Strict', @@ -33,3 +33,73 @@ describe('setCookiesToCookies', () => { expect(() => getCookies(['_csrf'])).toThrow(); }); }); + +describe('serializeDates', () => { + function isAsymmetricMatcher(x: unknown): x is typeof expect.any { + return ( + typeof x === 'object' && + x !== null && + typeof (x as { asymmetricMatch?: unknown }).asymmetricMatch === 'function' + ); + } + + test('returns primitives unchanged', () => { + expect(serializeDates(42)).toBe(42); + expect(serializeDates('hello')).toBe('hello'); + expect(serializeDates(true)).toBe(true); + }); + + test('converts Date to ISO string', () => { + const d = new Date('2020-01-01T00:00:00.000Z'); + expect(serializeDates(d)).toBe(d.toISOString()); + }); + + test('recursively converts nested objects with Date', () => { + const input = { + a: new Date('2021-05-05T05:05:05.000Z'), + b: { c: new Date('2022-06-06T06:06:06.000Z') } + }; + const output = serializeDates(input); + expect(output).toEqual({ + a: input.a.toISOString(), + b: { c: input.b.c.toISOString() } + }); + }); + + test('recursively converts arrays with Date and nested structures', () => { + const d1 = new Date('2020-02-02T02:02:02.000Z'); + const d2 = new Date('2023-03-03T03:03:03.000Z'); + const d3 = new Date('2024-04-04T04:04:04.000Z'); + const arr: [Date, { x: Date; y: Date[] }] = [d1, { x: d2, y: [d3] }]; + const out = serializeDates(arr); + expect(out).toEqual([ + d1.toISOString(), + { x: d2.toISOString(), y: [d3.toISOString()] } + ]); + }); + + test('handles null and undefined', () => { + expect(serializeDates(null)).toBeNull(); + expect(serializeDates(undefined)).toBeUndefined(); + }); + + test('preserves asymmetric matchers', () => { + type MatchersShape = { id: unknown; meta: { when: unknown } }; + const withMatchers: MatchersShape = { + id: expect.any(String) as unknown, + meta: { when: expect.stringMatching(/Z$/) as unknown } + }; + const serialized = serializeDates(withMatchers); + expect(isAsymmetricMatcher(serialized.id)).toBe(true); + expect(isAsymmetricMatcher(serialized.meta.when)).toBe(true); + + // Serializing `Date`s yield strings that match the same patterns + const body = { + id: 'abc', + meta: { when: new Date('2025-01-01T00:00:00.000Z') } + }; + const wrapped = serializeDates(body); + expect(typeof wrapped.id).toBe('string'); + expect(wrapped.meta.when).toMatch(/Z$/); + }); +}); diff --git a/api/vitest.utils.ts b/api/vitest.utils.ts index 15bd8829cd9..2c184702649 100644 --- a/api/vitest.utils.ts +++ b/api/vitest.utils.ts @@ -259,3 +259,52 @@ export function createFetchMock({ ok = true, body = {} } = {}) { }) ); } + +/** + * Utility type to recursively replace `Date` with `string`. + */ +export type ReplaceDates = T extends Date + ? string + : T extends (infer U)[] + ? ReplaceDates[] + : T extends Record + ? { [K in keyof T]: ReplaceDates } + : T; + +/** + * Recursively finds and converts Date objects to ISO strings while preserving shape. + */ +export function serializeDates(data: T): ReplaceDates { + if (data === null || data === undefined) { + return data as ReplaceDates; + } + + // Preserve Vitest/Jest asymmetric matchers (e.g., expect.any(Number)) + if ( + typeof data === 'object' && + data !== null && + typeof (data as { asymmetricMatch?: unknown }).asymmetricMatch === + 'function' + ) { + return data as unknown as ReplaceDates; + } + + if (data instanceof Date) { + return data.toISOString() as ReplaceDates; + } + + if (Array.isArray(data)) { + return (data as unknown[]).map(item => + serializeDates(item) + ) as ReplaceDates; + } + + if (typeof data === 'object') { + const entries = Object.entries(data as Record).map( + ([key, value]) => [key, serializeDates(value)] as const + ); + return Object.fromEntries(entries) as ReplaceDates; + } + + return data as ReplaceDates; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1412a8ea17c..d1ca18d5e25 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,13 +16,13 @@ importers: devDependencies: '@babel/eslint-parser': specifier: 7.26.5 - version: 7.26.5(@babel/core@7.23.7)(eslint@9.19.0) + version: 7.26.5(@babel/core@7.23.7)(eslint@9.19.0(jiti@2.6.1)) '@babel/preset-react': specifier: 7.26.3 version: 7.26.3(@babel/core@7.23.7) '@eslint/compat': specifier: ^1.2.6 - version: 1.2.9(eslint@9.19.0) + version: 1.2.9(eslint@9.19.0(jiti@2.6.1)) '@eslint/eslintrc': specifier: ^3.2.0 version: 3.3.1 @@ -49,49 +49,49 @@ importers: version: 5.14.9 '@typescript-eslint/eslint-plugin': specifier: 8.24.0 - version: 8.24.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0)(typescript@5.7.3))(eslint@9.19.0)(typescript@5.7.3) + version: 8.24.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3))(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) '@typescript-eslint/parser': specifier: 8.23.0 - version: 8.23.0(eslint@9.19.0)(typescript@5.7.3) + version: 8.23.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) '@vitest/eslint-plugin': specifier: ^1.3.12 - version: 1.3.12(eslint@9.19.0)(typescript@5.7.3)(vitest@3.2.4) + version: 1.3.12(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3)(vitest@3.2.4) debug: specifier: 4.3.4 version: 4.3.4(supports-color@8.1.1) eslint: specifier: 9.19.0 - version: 9.19.0 + version: 9.19.0(jiti@2.6.1) eslint-config-prettier: specifier: 10.0.1 - version: 10.0.1(eslint@9.19.0) + version: 10.0.1(eslint@9.19.0(jiti@2.6.1)) eslint-import-resolver-typescript: specifier: ^3.5.5 - version: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.19.0) + version: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.19.0(jiti@2.6.1)) eslint-plugin-filenames-simple: specifier: 0.9.0 - version: 0.9.0(eslint@9.19.0) + version: 0.9.0(eslint@9.19.0(jiti@2.6.1)) eslint-plugin-import: specifier: 2.31.0 - version: 2.31.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.19.0) + version: 2.31.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.19.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: 48.2.1 - version: 48.2.1(eslint@9.19.0) + version: 48.2.1(eslint@9.19.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: specifier: 6.10.2 - version: 6.10.2(eslint@9.19.0) + version: 6.10.2(eslint@9.19.0(jiti@2.6.1)) eslint-plugin-no-only-tests: specifier: 3.1.0 version: 3.1.0 eslint-plugin-react: specifier: 7.37.4 - version: 7.37.4(eslint@9.19.0) + version: 7.37.4(eslint@9.19.0(jiti@2.6.1)) eslint-plugin-react-hooks: specifier: 4.6.0 - version: 4.6.0(eslint@9.19.0) + version: 4.6.0(eslint@9.19.0(jiti@2.6.1)) eslint-plugin-testing-library: specifier: 7.1.1 - version: 7.1.1(eslint@9.19.0)(typescript@5.7.3) + version: 7.1.1(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) globals: specifier: ^15.14.0 version: 15.15.0 @@ -133,7 +133,7 @@ importers: version: 5.7.3 typescript-eslint: specifier: ^8.23.0 - version: 8.33.0(eslint@9.19.0)(typescript@5.7.3) + version: 8.33.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) webpack-bundle-analyzer: specifier: 4.10.1 version: 4.10.1 @@ -171,8 +171,8 @@ importers: specifier: 1.3.1 version: 1.3.1 '@prisma/client': - specifier: 5.5.2 - version: 5.5.2(prisma@5.5.2) + specifier: 6.16.2 + version: 6.16.2(prisma@6.16.2(typescript@5.8.2))(typescript@5.8.2) '@sentry/node': specifier: 9.1.0 version: 9.1.0 @@ -277,8 +277,8 @@ importers: specifier: ^2.7.0 version: 2.8.7(@types/node@20.12.8)(typescript@5.8.2) prisma: - specifier: 5.5.2 - version: 5.5.2 + specifier: 6.16.2 + version: 6.16.2(typescript@5.8.2) supertest: specifier: 6.3.3 version: 6.3.3 @@ -287,7 +287,7 @@ importers: version: 4.19.1 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + version: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) client: dependencies: @@ -693,7 +693,7 @@ importers: version: 13.0.4 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.2.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + version: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.2.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) webpack: specifier: 5.90.3 version: 5.90.3(webpack-cli@4.10.0) @@ -762,7 +762,7 @@ importers: version: 4.0.4 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jsdom@16.7.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + version: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@16.7.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) shared: devDependencies: @@ -771,7 +771,7 @@ importers: version: 3.2.4(vitest@3.2.4) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + version: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) tools/challenge-editor/api: dependencies: @@ -872,7 +872,7 @@ importers: version: 5.2.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.2.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + version: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.2.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) tools/challenge-parser: dependencies: @@ -972,7 +972,7 @@ importers: version: 3.0.4 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + version: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) tools/client-plugins/browser-scripts: dependencies: @@ -1068,7 +1068,7 @@ importers: version: 3.6.0 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + version: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) tools/scripts/lint: devDependencies: @@ -1077,7 +1077,7 @@ importers: version: 3.2.4(vitest@3.2.4) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + version: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) tools/scripts/seed: devDependencies: @@ -3565,20 +3565,35 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@prisma/client@5.5.2': - resolution: {integrity: sha512-54XkqR8M+fxbzYqe+bIXimYnkkcGqgOh0dn0yWtIk6CQT4IUCAvNFNcQZwk2KqaLU+/1PHTSWrcHtx4XjluR5w==} - engines: {node: '>=16.13'} + '@prisma/client@6.16.2': + resolution: {integrity: sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw==} + engines: {node: '>=18.18'} peerDependencies: prisma: '*' + typescript: '>=5.1.0' peerDependenciesMeta: prisma: optional: true + typescript: + optional: true - '@prisma/engines-version@5.5.1-1.aebc046ce8b88ebbcb45efe31cbe7d06fd6abc0a': - resolution: {integrity: sha512-O+qHFnZvAyOFk1tUco2/VdiqS0ym42a3+6CYLScllmnpbyiTplgyLt2rK/B9BTjYkSHjrgMhkG47S0oqzdIckA==} + '@prisma/config@6.16.2': + resolution: {integrity: sha512-mKXSUrcqXj0LXWPmJsK2s3p9PN+aoAbyMx7m5E1v1FufofR1ZpPoIArjjzOIm+bJRLLvYftoNYLx1tbHgF9/yg==} - '@prisma/engines@5.5.2': - resolution: {integrity: sha512-Be5hoNF8k+lkB3uEMiCHbhbfF6aj1GnrTBnn5iYFT7GEr3TsOEp1soviEcBR0tYCgHbxjcIxJMhdbvxALJhAqg==} + '@prisma/debug@6.16.2': + resolution: {integrity: sha512-bo4/gA/HVV6u8YK2uY6glhNsJ7r+k/i5iQ9ny/3q5bt9ijCj7WMPUwfTKPvtEgLP+/r26Z686ly11hhcLiQ8zA==} + + '@prisma/engines-version@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43': + resolution: {integrity: sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA==} + + '@prisma/engines@6.16.2': + resolution: {integrity: sha512-7yf3AjfPUgsg/l7JSu1iEhsmZZ/YE00yURPjTikqm2z4btM0bCl2coFtTGfeSOWbQMmq45Jab+53yGUIAT1sjA==} + + '@prisma/fetch-engine@6.16.2': + resolution: {integrity: sha512-wPnZ8DMRqpgzye758ZvfAMiNJRuYpz+rhgEBZi60ZqDIgOU2694oJxiuu3GKFeYeR/hXxso4/2oBC243t/whxQ==} + + '@prisma/get-platform@6.16.2': + resolution: {integrity: sha512-U/P36Uke5wS7r1+omtAgJpEB94tlT4SdlgaeTc6HVTTT93pXj7zZ+B/cZnmnvjcNPfWddgoDx8RLjmQwqGDYyA==} '@prisma/instrumentation@6.2.1': resolution: {integrity: sha512-QrcWRAwNHXX4nHXB+Q94nfm701gPQsR4zkaxYV6qCiENopRi8yYvXt6FNIvxbuwEiWW5Zid6DoWwIsBKJ/5r5w==} @@ -5990,6 +6005,14 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + c12@3.1.0: + resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -6141,6 +6164,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -6168,6 +6195,9 @@ packages: cipher-base@1.0.4: resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==} + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + cjs-module-lexer@1.2.3: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} @@ -6338,6 +6368,9 @@ packages: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} engines: {'0': node >= 0.8} + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + configstore@5.0.1: resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==} engines: {node: '>=8'} @@ -6345,6 +6378,10 @@ packages: confusing-browser-globals@1.0.11: resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + console-browserify@1.2.0: resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==} @@ -6741,6 +6778,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -6778,6 +6819,9 @@ packages: defined@1.0.1: resolution: {integrity: sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + degenerator@5.0.1: resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} engines: {node: '>= 14'} @@ -6809,6 +6853,9 @@ packages: des.js@1.1.0: resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -6963,6 +7010,10 @@ packages: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + dotenv@8.6.0: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} @@ -6989,6 +7040,9 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + effect@3.16.12: + resolution: {integrity: sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==} + electron-to-chromium@1.4.622: resolution: {integrity: sha512-GZ47DEy0Gm2Z8RVG092CkFvX7SdotG57c4YZOe8W8qD4rOmk3plgeNmiLVRHP/Liqj1wRiY3uUUod9vb9hnxZA==} @@ -7008,6 +7062,10 @@ packages: resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} engines: {node: '>= 4'} + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -7547,6 +7605,9 @@ packages: resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} engines: {node: '>= 0.10.0'} + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} @@ -7578,6 +7639,10 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + fast-copy@2.1.7: resolution: {integrity: sha512-ozrGwyuCTAy7YgFCua8rmqmytECYk/JYAMXcswOcm0qvGoE3tPb7ivBeIHTOK2DiapBhDZgacIhzhQIKU5TCfA==} @@ -8116,6 +8181,10 @@ packages: resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} engines: {node: '>=0.10.0'} + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + git-up@4.0.5: resolution: {integrity: sha512-YUvVDg/vX3d0syBsk/CKUTib0srcQME0JyHkL5BaYdwLsiCslPWmDSi8PUMo9pXYjrryMcmsCoCgsTpSCJEQaA==} @@ -9211,6 +9280,10 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + joi-objectid@3.0.1: resolution: {integrity: sha512-V/3hbTlGpvJ03Me6DJbdBI08hBTasFOmipsauOsxOSnsF1blxV537WTl1zPwbfcKle4AK0Ma4OPnzMH4LlvTpQ==} @@ -10346,6 +10419,9 @@ packages: node-eta@0.9.0: resolution: {integrity: sha512-mTCTZk29tmX1OGfVkPt63H3c3VqXrI2Kvua98S7iUIB/Gbp0MNw05YtUomxQIxnnKMyRIIuY9izPcFixzhSBrA==} + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-fetch@2.6.1: resolution: {integrity: sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==} engines: {node: 4.x || >=6.0.0} @@ -10443,6 +10519,11 @@ packages: nwsapi@2.2.7: resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} + nypm@0.6.2: + resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==} + engines: {node: ^14.16.0 || >=16.10.0} + hasBin: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -10520,6 +10601,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + on-exit-leak-free@2.1.0: resolution: {integrity: sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==} @@ -10803,6 +10887,9 @@ packages: pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} @@ -10877,6 +10964,9 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + pkg-up@3.1.0: resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} engines: {node: '>=8'} @@ -11214,10 +11304,15 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - prisma@5.5.2: - resolution: {integrity: sha512-WQtG6fevOL053yoPl6dbHV+IWgKo25IRN4/pwAGqcWmg7CrtoCzvbDbN9fXUc7QS2KK0LimHIqLsaCOX/vHl8w==} - engines: {node: '>=16.13'} + prisma@6.16.2: + resolution: {integrity: sha512-aRvldGE5UUJTtVmFiH3WfNFNiqFlAtePUxcI0UEGlnXCX7DqhiMT5TRYwncHFeA/Reca5W6ToXXyCMTeFPdSXA==} + engines: {node: '>=18.18'} hasBin: true + peerDependencies: + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true prismjs@1.29.0: resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} @@ -11330,9 +11425,12 @@ packages: puppeteer@24.10.0: resolution: {integrity: sha512-Oua9VkGpj0S2psYu5e6mCer6W9AU9POEQh22wRgSXnLXASGH+MwLUVWgLCLeP9QPHHcJ7tySUlg4Sa9OJmaLpw==} engines: {node: '>=18'} - deprecated: < 24.10.2 is no longer supported + deprecated: < 24.15.0 is no longer supported hasBin: true + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pyodide@0.23.3: resolution: {integrity: sha512-t95Nu73ENjwoRhThmxvZIHMD7GXTJ3uOt/E0sJ1TxjBvoU/qPys4SV08FtZBMEnpMRKFzE4uecvx2c0qybSZhw==} @@ -11425,6 +11523,9 @@ packages: peerDependencies: webpack: ^4.0.0 || ^5.0.0 + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -11658,6 +11759,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -12833,6 +12938,9 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} @@ -14699,11 +14807,11 @@ snapshots: eslint-visitor-keys: 2.1.0 semver: 6.3.1 - '@babel/eslint-parser@7.26.5(@babel/core@7.23.7)(eslint@9.19.0)': + '@babel/eslint-parser@7.26.5(@babel/core@7.23.7)(eslint@9.19.0(jiti@2.6.1))': dependencies: '@babel/core': 7.23.7 '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) eslint-visitor-keys: 2.1.0 semver: 6.3.1 @@ -16769,21 +16877,21 @@ snapshots: '@esbuild/win32-x64@0.25.9': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@9.19.0)': + '@eslint-community/eslint-utils@4.4.0(eslint@9.19.0(jiti@2.6.1))': dependencies: - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.7.0(eslint@9.19.0)': + '@eslint-community/eslint-utils@4.7.0(eslint@9.19.0(jiti@2.6.1))': dependencies: - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/compat@1.2.9(eslint@9.19.0)': + '@eslint/compat@1.2.9(eslint@9.19.0(jiti@2.6.1))': optionalDependencies: - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) '@eslint/config-array@0.19.2': dependencies: @@ -17727,15 +17835,40 @@ snapshots: '@polka/url@1.0.0-next.29': {} - '@prisma/client@5.5.2(prisma@5.5.2)': - dependencies: - '@prisma/engines-version': 5.5.1-1.aebc046ce8b88ebbcb45efe31cbe7d06fd6abc0a + '@prisma/client@6.16.2(prisma@6.16.2(typescript@5.8.2))(typescript@5.8.2)': optionalDependencies: - prisma: 5.5.2 + prisma: 6.16.2(typescript@5.8.2) + typescript: 5.8.2 - '@prisma/engines-version@5.5.1-1.aebc046ce8b88ebbcb45efe31cbe7d06fd6abc0a': {} + '@prisma/config@6.16.2': + dependencies: + c12: 3.1.0 + deepmerge-ts: 7.1.5 + effect: 3.16.12 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast - '@prisma/engines@5.5.2': {} + '@prisma/debug@6.16.2': {} + + '@prisma/engines-version@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43': {} + + '@prisma/engines@6.16.2': + dependencies: + '@prisma/debug': 6.16.2 + '@prisma/engines-version': 6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43 + '@prisma/fetch-engine': 6.16.2 + '@prisma/get-platform': 6.16.2 + + '@prisma/fetch-engine@6.16.2': + dependencies: + '@prisma/debug': 6.16.2 + '@prisma/engines-version': 6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43 + '@prisma/get-platform': 6.16.2 + + '@prisma/get-platform@6.16.2': + dependencies: + '@prisma/debug': 6.16.2 '@prisma/instrumentation@6.2.1(@opentelemetry/api@1.9.0)': dependencies: @@ -18688,7 +18821,7 @@ snapshots: dom-accessibility-api: 0.6.3 lodash: 4.17.21 redent: 3.0.0 - vitest: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.7.3))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + vitest: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.7.3))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) '@testing-library/jest-dom@6.8.0(vitest@3.2.4)': dependencies: @@ -18698,7 +18831,7 @@ snapshots: dom-accessibility-api: 0.6.3 picocolors: 1.1.1 redent: 3.0.0 - vitest: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.2.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + vitest: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.2.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) '@testing-library/react-hooks@8.0.1(@types/react@17.0.83)(react-dom@17.0.2(react@17.0.2))(react-test-renderer@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: @@ -19252,15 +19385,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.24.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0)(typescript@5.7.3))(eslint@9.19.0)(typescript@5.7.3)': + '@typescript-eslint/eslint-plugin@8.24.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3))(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.23.0(eslint@9.19.0)(typescript@5.7.3) + '@typescript-eslint/parser': 8.23.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) '@typescript-eslint/scope-manager': 8.24.0 - '@typescript-eslint/type-utils': 8.24.0(eslint@9.19.0)(typescript@5.7.3) - '@typescript-eslint/utils': 8.24.0(eslint@9.19.0)(typescript@5.7.3) + '@typescript-eslint/type-utils': 8.24.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) + '@typescript-eslint/utils': 8.24.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.24.0 - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -19269,15 +19402,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.33.0(@typescript-eslint/parser@8.33.0(eslint@9.19.0)(typescript@5.7.3))(eslint@9.19.0)(typescript@5.7.3)': + '@typescript-eslint/eslint-plugin@8.33.0(@typescript-eslint/parser@8.33.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3))(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.33.0(eslint@9.19.0)(typescript@5.7.3) + '@typescript-eslint/parser': 8.33.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) '@typescript-eslint/scope-manager': 8.33.0 - '@typescript-eslint/type-utils': 8.33.0(eslint@9.19.0)(typescript@5.7.3) - '@typescript-eslint/utils': 8.33.0(eslint@9.19.0)(typescript@5.7.3) + '@typescript-eslint/type-utils': 8.33.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) + '@typescript-eslint/utils': 8.33.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.33.0 - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -19324,26 +19457,26 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.23.0(eslint@9.19.0)(typescript@5.7.3)': + '@typescript-eslint/parser@8.23.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3)': dependencies: '@typescript-eslint/scope-manager': 8.23.0 '@typescript-eslint/types': 8.23.0 '@typescript-eslint/typescript-estree': 8.23.0(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.23.0 debug: 4.3.4(supports-color@8.1.1) - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.33.0(eslint@9.19.0)(typescript@5.7.3)': + '@typescript-eslint/parser@8.33.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3)': dependencies: '@typescript-eslint/scope-manager': 8.33.0 '@typescript-eslint/types': 8.33.0 '@typescript-eslint/typescript-estree': 8.33.0(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.33.0 debug: 4.3.4(supports-color@8.1.1) - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -19391,23 +19524,23 @@ snapshots: dependencies: typescript: 5.7.3 - '@typescript-eslint/type-utils@8.24.0(eslint@9.19.0)(typescript@5.7.3)': + '@typescript-eslint/type-utils@8.24.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3)': dependencies: '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3) - '@typescript-eslint/utils': 8.24.0(eslint@9.19.0)(typescript@5.7.3) + '@typescript-eslint/utils': 8.24.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) debug: 4.3.4(supports-color@8.1.1) - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) ts-api-utils: 2.0.1(typescript@5.7.3) typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.33.0(eslint@9.19.0)(typescript@5.7.3)': + '@typescript-eslint/type-utils@8.33.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3)': dependencies: '@typescript-eslint/typescript-estree': 8.33.0(typescript@5.7.3) - '@typescript-eslint/utils': 8.33.0(eslint@9.19.0)(typescript@5.7.3) + '@typescript-eslint/utils': 8.33.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) debug: 4.3.4(supports-color@8.1.1) - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.7.3) typescript: 5.7.3 transitivePeerDependencies: @@ -19516,35 +19649,35 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.24.0(eslint@9.19.0)(typescript@5.7.3)': + '@typescript-eslint/utils@8.24.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.19.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.19.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.24.0 '@typescript-eslint/types': 8.24.0 '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3) - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.26.1(eslint@9.19.0)(typescript@5.7.3)': + '@typescript-eslint/utils@8.26.1(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.19.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.19.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.26.1 '@typescript-eslint/types': 8.26.1 '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.7.3) - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.33.0(eslint@9.19.0)(typescript@5.7.3)': + '@typescript-eslint/utils@8.33.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.19.0) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.19.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.33.0 '@typescript-eslint/types': 8.33.0 '@typescript-eslint/typescript-estree': 8.33.0(typescript@5.7.3) - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -19666,14 +19799,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.3.12(eslint@9.19.0)(typescript@5.7.3)(vitest@3.2.4)': + '@vitest/eslint-plugin@1.3.12(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3)(vitest@3.2.4)': dependencies: '@typescript-eslint/scope-manager': 8.44.0 - '@typescript-eslint/utils': 8.33.0(eslint@9.19.0)(typescript@5.7.3) - eslint: 9.19.0 + '@typescript-eslint/utils': 8.33.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) + eslint: 9.19.0(jiti@2.6.1) optionalDependencies: typescript: 5.7.3 - vitest: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.7.3))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + vitest: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.7.3))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -19685,32 +19818,32 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.8.7(@types/node@20.12.8)(typescript@5.2.2))(vite@7.1.3(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0))': + '@vitest/mocker@3.2.4(msw@2.8.7(@types/node@20.12.8)(typescript@5.2.2))(vite@7.1.3(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: msw: 2.8.7(@types/node@20.12.8)(typescript@5.2.2) - vite: 7.1.3(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + vite: 7.1.3(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) - '@vitest/mocker@3.2.4(msw@2.8.7(@types/node@20.12.8)(typescript@5.7.3))(vite@7.1.3(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0))': + '@vitest/mocker@3.2.4(msw@2.8.7(@types/node@20.12.8)(typescript@5.7.3))(vite@7.1.3(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: msw: 2.8.7(@types/node@20.12.8)(typescript@5.7.3) - vite: 7.1.3(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + vite: 7.1.3(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) - '@vitest/mocker@3.2.4(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(vite@7.1.3(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0))': + '@vitest/mocker@3.2.4(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(vite@7.1.3(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: msw: 2.8.7(@types/node@20.12.8)(typescript@5.8.2) - vite: 7.1.3(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + vite: 7.1.3(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -19741,7 +19874,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + vitest: 3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -20854,6 +20987,21 @@ snapshots: bytes@3.1.2: {} + c12@3.1.0: + dependencies: + chokidar: 4.0.3 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 16.6.1 + exsolve: 1.0.7 + giget: 2.0.0 + jiti: 2.6.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.3.0 + rc9: 2.1.2 + cac@6.7.14: {} cache-base@1.0.1: @@ -21048,6 +21196,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + chownr@1.1.4: {} chrome-trace-event@1.0.3: {} @@ -21074,6 +21226,10 @@ snapshots: inherits: 2.0.4 safe-buffer: 5.2.1 + citty@0.1.6: + dependencies: + consola: 3.4.2 + cjs-module-lexer@1.2.3: {} class-utils@0.3.6: @@ -21251,6 +21407,8 @@ snapshots: readable-stream: 2.3.8 typedarray: 0.0.6 + confbox@0.2.2: {} + configstore@5.0.1: dependencies: dot-prop: 5.3.0 @@ -21262,6 +21420,8 @@ snapshots: confusing-browser-globals@1.0.11: {} + consola@3.4.2: {} + console-browserify@1.2.0: {} constants-browserify@1.0.0: {} @@ -21717,6 +21877,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge-ts@7.1.5: {} + deepmerge@4.3.1: {} defaults@1.0.4: @@ -21754,6 +21916,8 @@ snapshots: defined@1.0.1: {} + defu@6.1.4: {} + degenerator@5.0.1: dependencies: ast-types: 0.13.4 @@ -21791,6 +21955,8 @@ snapshots: inherits: 2.0.4 minimalistic-assert: 1.0.1 + destr@2.0.5: {} + destroy@1.2.0: {} detect-libc@1.0.3: {} @@ -21970,6 +22136,8 @@ snapshots: dotenv@16.4.5: {} + dotenv@16.6.1: {} + dotenv@8.6.0: {} dunder-proto@1.0.1: @@ -21994,6 +22162,11 @@ snapshots: ee-first@1.1.1: {} + effect@3.16.12: + dependencies: + '@standard-schema/spec': 1.0.0 + fast-check: 3.23.2 + electron-to-chromium@1.4.622: {} electron-to-chromium@1.4.685: {} @@ -22014,6 +22187,8 @@ snapshots: emojis-list@3.0.0: {} + empathic@2.0.0: {} + encodeurl@1.0.2: {} end-of-stream@1.4.4: @@ -22407,9 +22582,9 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-prettier@10.0.1(eslint@9.19.0): + eslint-config-prettier@10.0.1(eslint@9.19.0(jiti@2.6.1)): dependencies: - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) eslint-config-react-app@6.0.0(@typescript-eslint/eslint-plugin@4.33.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.2.2))(eslint@7.32.0)(typescript@5.2.2))(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.2.2))(babel-eslint@10.1.0(eslint@7.32.0))(eslint-plugin-flowtype@5.10.0(eslint@7.32.0))(eslint-plugin-import@2.28.1(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.2.2))(eslint-import-resolver-typescript@3.10.1)(eslint@7.32.0))(eslint-plugin-jsx-a11y@6.7.1(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.0(eslint@7.32.0))(eslint-plugin-react@7.33.2(eslint@7.32.0))(eslint-plugin-testing-library@3.9.0(eslint@7.32.0)(typescript@5.2.2))(eslint@7.32.0)(typescript@5.2.2): dependencies: @@ -22435,29 +22610,29 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.19.0): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.19.0(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) get-tsconfig: 4.10.1 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.14 unrs-resolver: 1.7.8 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.19.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.19.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.19.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.19.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.23.0(eslint@9.19.0)(typescript@5.7.3) - eslint: 9.19.0 + '@typescript-eslint/parser': 8.23.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) + eslint: 9.19.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.19.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.19.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -22468,13 +22643,13 @@ snapshots: '@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@5.2.2) eslint: 7.32.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.19.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.19.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-filenames-simple@0.9.0(eslint@9.19.0): + eslint-plugin-filenames-simple@0.9.0(eslint@9.19.0(jiti@2.6.1)): dependencies: - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) pluralize: 8.0.0 eslint-plugin-flowtype@5.10.0(eslint@7.32.0): @@ -22524,7 +22699,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.19.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.19.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -22533,9 +22708,9 @@ snapshots: array.prototype.flatmap: 1.3.2 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.19.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.19.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -22547,20 +22722,20 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.23.0(eslint@9.19.0)(typescript@5.7.3) + '@typescript-eslint/parser': 8.23.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsdoc@48.2.1(eslint@9.19.0): + eslint-plugin-jsdoc@48.2.1(eslint@9.19.0(jiti@2.6.1)): dependencies: '@es-joy/jsdoccomment': 0.42.0 are-docs-informative: 0.0.2 comment-parser: 1.4.1 debug: 4.3.4(supports-color@8.1.1) escape-string-regexp: 4.0.0 - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) esquery: 1.5.0 is-builtin-module: 3.2.1 semver: 7.6.0 @@ -22568,7 +22743,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.19.0): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.19.0(jiti@2.6.1)): dependencies: aria-query: 5.3.2 array-includes: 3.1.8 @@ -22578,7 +22753,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -22613,9 +22788,9 @@ snapshots: dependencies: eslint: 7.32.0 - eslint-plugin-react-hooks@4.6.0(eslint@9.19.0): + eslint-plugin-react-hooks@4.6.0(eslint@9.19.0(jiti@2.6.1)): dependencies: - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) eslint-plugin-react@7.33.2(eslint@7.32.0): dependencies: @@ -22637,7 +22812,7 @@ snapshots: semver: 6.3.1 string.prototype.matchall: 4.0.10 - eslint-plugin-react@7.37.4(eslint@9.19.0): + eslint-plugin-react@7.37.4(eslint@9.19.0(jiti@2.6.1)): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -22645,7 +22820,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.19.0 + eslint: 9.19.0(jiti@2.6.1) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -22668,11 +22843,11 @@ snapshots: - typescript optional: true - eslint-plugin-testing-library@7.1.1(eslint@9.19.0)(typescript@5.7.3): + eslint-plugin-testing-library@7.1.1(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3): dependencies: '@typescript-eslint/scope-manager': 8.26.1 - '@typescript-eslint/utils': 8.26.1(eslint@9.19.0)(typescript@5.7.3) - eslint: 9.19.0 + '@typescript-eslint/utils': 8.26.1(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) + eslint: 9.19.0(jiti@2.6.1) transitivePeerDependencies: - supports-color - typescript @@ -22762,9 +22937,9 @@ snapshots: transitivePeerDependencies: - supports-color - eslint@9.19.0: + eslint@9.19.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.19.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.19.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.19.2 '@eslint/core': 0.10.0 @@ -22798,6 +22973,8 @@ snapshots: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.3 + optionalDependencies: + jiti: 2.6.1 transitivePeerDependencies: - supports-color @@ -22972,6 +23149,8 @@ snapshots: transitivePeerDependencies: - supports-color + exsolve@1.0.7: {} + ext@1.7.0: dependencies: type: 2.7.2 @@ -23018,6 +23197,10 @@ snapshots: transitivePeerDependencies: - supports-color + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + fast-copy@2.1.7: {} fast-copy@3.0.1: {} @@ -23998,6 +24181,15 @@ snapshots: get-value@2.0.6: {} + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.4 + node-fetch-native: 1.6.7 + nypm: 0.6.2 + pathe: 2.0.3 + git-up@4.0.5: dependencies: is-ssh: 1.4.0 @@ -25236,6 +25428,8 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + jiti@2.6.1: {} + joi-objectid@3.0.1: {} joi@17.12.2: @@ -26799,6 +26993,8 @@ snapshots: node-eta@0.9.0: {} + node-fetch-native@1.6.7: {} + node-fetch@2.6.1: {} node-fetch@2.7.0: @@ -26879,6 +27075,14 @@ snapshots: nwsapi@2.2.7: {} + nypm@0.6.2: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + pathe: 2.0.3 + pkg-types: 2.3.0 + tinyexec: 1.0.1 + object-assign@4.1.1: {} object-copy@0.1.0: @@ -26980,6 +27184,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + ohash@2.0.11: {} + on-exit-leak-free@2.1.0: {} on-finished@2.4.1: @@ -27286,6 +27492,8 @@ snapshots: pend@1.2.0: {} + perfect-debounce@1.0.0: {} + pg-int8@1.0.1: {} pg-protocol@1.8.0: {} @@ -27368,6 +27576,12 @@ snapshots: dependencies: find-up: 4.1.0 + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + pkg-up@3.1.0: dependencies: find-up: 3.0.0 @@ -27693,9 +27907,14 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.2.0 - prisma@5.5.2: + prisma@6.16.2(typescript@5.8.2): dependencies: - '@prisma/engines': 5.5.2 + '@prisma/config': 6.16.2 + '@prisma/engines': 6.16.2 + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - magicast prismjs@1.29.0: {} @@ -27857,6 +28076,8 @@ snapshots: - typescript - utf-8-validate + pure-rand@6.1.0: {} + pyodide@0.23.3: dependencies: base-64: 1.0.0 @@ -27960,6 +28181,11 @@ snapshots: schema-utils: 3.3.0 webpack: 5.90.3(webpack-cli@4.10.0) + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.5 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -28259,6 +28485,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@4.1.2: {} + real-require@0.2.0: {} rechoir@0.6.2: @@ -29794,6 +30022,8 @@ snapshots: tinyexec@0.3.2: {} + tinyexec@1.0.1: {} + tinyglobby@0.2.14: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -30087,12 +30317,12 @@ snapshots: dependencies: typescript-logic: 0.0.0 - typescript-eslint@8.33.0(eslint@9.19.0)(typescript@5.7.3): + typescript-eslint@8.33.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.33.0(@typescript-eslint/parser@8.33.0(eslint@9.19.0)(typescript@5.7.3))(eslint@9.19.0)(typescript@5.7.3) - '@typescript-eslint/parser': 8.33.0(eslint@9.19.0)(typescript@5.7.3) - '@typescript-eslint/utils': 8.33.0(eslint@9.19.0)(typescript@5.7.3) - eslint: 9.19.0 + '@typescript-eslint/eslint-plugin': 8.33.0(@typescript-eslint/parser@8.33.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3))(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) + '@typescript-eslint/parser': 8.33.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) + '@typescript-eslint/utils': 8.33.0(eslint@9.19.0(jiti@2.6.1))(typescript@5.7.3) + eslint: 9.19.0(jiti@2.6.1) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -30528,13 +30758,13 @@ snapshots: unist-util-stringify-position: 2.0.3 vfile-message: 2.0.4 - vite-node@3.2.4(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0): + vite-node@3.2.4(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.3(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + vite: 7.1.3(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -30559,7 +30789,7 @@ snapshots: fsevents: 2.3.3 terser: 5.28.1 - vite@7.1.3(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0): + vite@7.1.3(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -30570,6 +30800,7 @@ snapshots: optionalDependencies: '@types/node': 20.12.8 fsevents: 2.3.3 + jiti: 2.6.1 terser: 5.28.1 tsx: 4.19.1 yaml: 2.8.0 @@ -30596,11 +30827,11 @@ snapshots: - debug - typescript - vitest@3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jsdom@16.7.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0): + vitest@3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@16.7.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(vite@7.1.3(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(vite@7.1.3(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -30618,8 +30849,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.3(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + vite: 7.1.3(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + vite-node: 3.2.4(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.12.8 @@ -30639,11 +30870,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.2.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0): + vitest@3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.2.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.8.7(@types/node@20.12.8)(typescript@5.2.2))(vite@7.1.3(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(msw@2.8.7(@types/node@20.12.8)(typescript@5.2.2))(vite@7.1.3(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -30661,8 +30892,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.3(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + vite: 7.1.3(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + vite-node: 3.2.4(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.12.8 @@ -30682,11 +30913,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.7.3))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0): + vitest@3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.7.3))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.8.7(@types/node@20.12.8)(typescript@5.7.3))(vite@7.1.3(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(msw@2.8.7(@types/node@20.12.8)(typescript@5.7.3))(vite@7.1.3(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -30704,8 +30935,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.3(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + vite: 7.1.3(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + vite-node: 3.2.4(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.12.8 @@ -30725,11 +30956,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0): + vitest@3.2.4(@types/node@20.12.8)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(vite@7.1.3(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(msw@2.8.7(@types/node@20.12.8)(typescript@5.8.2))(vite@7.1.3(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -30747,8 +30978,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.3(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@20.12.8)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + vite: 7.1.3(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) + vite-node: 3.2.4(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.12.8