diff --git a/src/backend/index.ts b/src/backend/index.ts index ffe251ec65..8cde2f4f5a 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -25,6 +25,7 @@ import statisticsRouter from './src/routes/statistics.routes'; import retrospectiveRouter from './src/routes/retrospective.routes'; import partsRouter from './src/routes/parts.routes'; import financeRouter from './src/routes/finance.routes'; +import rulesRouter from './src/routes/rules.routes'; const app = express(); @@ -87,6 +88,7 @@ app.use('/statistics', statisticsRouter); app.use('/retrospective', retrospectiveRouter); app.use('/parts', partsRouter); app.use('/finance', financeRouter); +app.use('/rules', rulesRouter); app.use('/', (_req, res) => { res.status(200).json('Welcome to FinishLine'); }); diff --git a/src/backend/package.json b/src/backend/package.json index 92ec84d984..8b046fd7e1 100644 --- a/src/backend/package.json +++ b/src/backend/package.json @@ -32,6 +32,7 @@ "jsonwebtoken": "^8.5.1", "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.1", + "pdf-parse-new": "^1.4.1", "prisma": "^6.2.1", "shared": "1.0.0" }, diff --git a/src/backend/src/controllers/rules.controllers.ts b/src/backend/src/controllers/rules.controllers.ts new file mode 100644 index 0000000000..1149c961c7 --- /dev/null +++ b/src/backend/src/controllers/rules.controllers.ts @@ -0,0 +1,339 @@ +import { NextFunction, Request, Response } from 'express'; +import RulesService from '../services/rules.services'; +import { ProjectRule, Rule, Ruleset } from 'shared'; +import { HttpException } from '../utils/errors.utils'; + +export default class RulesController { + static async getActiveRuleset(req: Request, res: Response, next: NextFunction) { + try { + const { rulesetTypeId } = req.params; + const rulesetType = await RulesService.getActiveRuleset(req.currentUser, rulesetTypeId, req.organization); + res.status(200).json(rulesetType); + } catch (error: unknown) { + next(error); + } + } + + static async getRulesetById(req: Request, res: Response, next: NextFunction) { + try { + const { rulesetId } = req.params; + const ruleset = await RulesService.getRulesetById(rulesetId, req.organization.organizationId); + res.status(200).json(ruleset); + } catch (error: unknown) { + next(error); + } + } + + static async createRule(req: Request, res: Response, next: NextFunction) { + try { + const { ruleCode, ruleContent, rulesetId, parentRuleId, referencedRules, imageFileIds } = req.body; + + const rule = await RulesService.createRule( + req.currentUser, + ruleCode, + ruleContent, + rulesetId, + req.organization, + parentRuleId, + referencedRules || [], + imageFileIds || [] + ); + + res.status(201).json(rule); + } catch (error: unknown) { + next(error); + } + } + + static async deleteRule(req: Request, res: Response, next: NextFunction) { + try { + const { ruleId } = req.params; + const deletedRule = await RulesService.deleteRule(ruleId, req.currentUser, req.organization); + res.status(200).json(deletedRule); + } catch (error: unknown) { + next(error); + } + } + + static async createRulesetType(req: Request, res: Response, next: NextFunction) { + try { + const { name } = req.body; + const rulesetType = await RulesService.createRulesetType(req.currentUser, name, req.organization); + res.status(200).json(rulesetType); + } catch (error: unknown) { + next(error); + } + } + + static async createProjectRule(req: Request, res: Response, next: NextFunction) { + try { + const { ruleId, projectId } = req.body; + const projectRule: ProjectRule = await RulesService.createProjectRule( + req.currentUser, + req.organization, + ruleId, + projectId + ); + + res.status(200).json(projectRule); + } catch (error: unknown) { + next(error); + } + } + + static async editRule(req: Request, res: Response, next: NextFunction) { + try { + const { ruleId } = req.params; + const { ruleContent, ruleCode, imageFileIds, parentRuleId } = req.body; + + const rule = await RulesService.editRule( + req.currentUser, + ruleContent, + ruleId, + ruleCode, + imageFileIds, + req.organization, + parentRuleId + ); + res.status(200).json(rule); + } catch (error: unknown) { + next(error); + } + } + + static async getAllRulesetTypes(req: Request, res: Response, next: NextFunction) { + try { + const rulesets = await RulesService.getAllRulesetTypes(req.organization); + res.status(200).json(rulesets); + } catch (error: unknown) { + next(error); + } + } + + static async getRulesetsByRulesetType(req: Request, res: Response, next: NextFunction) { + try { + const { rulesetTypeId } = req.params; + const rulesets = await RulesService.getRulesetsByRulesetType(rulesetTypeId, req.organization.organizationId); + res.status(200).json(rulesets); + } catch (error: unknown) { + next(error); + } + } + + static async getRulesetType(req: Request, res: Response, next: NextFunction) { + try { + const { rulesetTypeId } = req.params; + const rulesetType = await RulesService.getRulesetType(rulesetTypeId, req.organization.organizationId); + res.status(200).json(rulesetType); + } catch (error: unknown) { + next(error); + } + } + + static async deleteRuleset(req: Request, res: Response, next: NextFunction) { + try { + const { rulesetId } = req.params; + const ruleset = await RulesService.deleteRuleset(rulesetId, req.currentUser.userId, req.organization.organizationId); + res.status(200).json(ruleset); + } catch (error: unknown) { + next(error); + } + } + + static async deleteProjectRule(req: Request, res: Response, next: NextFunction) { + try { + const { projectRuleId } = req.params; + const deletedProjectRule = await RulesService.deleteProjectRule(projectRuleId, req.currentUser, req.organization); + res.status(200).json(deletedProjectRule); + } catch (error: unknown) { + next(error); + } + } + + static async editProjectRuleStatus(req: Request, res: Response, next: NextFunction) { + try { + const { projectRuleId } = req.params; + const { newStatus } = req.body; + + const projectRule: ProjectRule = await RulesService.editProjectRuleStatus( + req.currentUser, + req.organization, + projectRuleId, + newStatus + ); + + res.status(200).json(projectRule); + } catch (error: unknown) { + next(error); + } + } + + static async toggleRuleTeam(req: Request, res: Response, next: NextFunction) { + try { + const { ruleId } = req.params; + const { teamId } = req.body; + + const changedRule = await RulesService.toggleRuleTeam(ruleId, teamId, req.currentUser, req.organization); + + res.status(200).json(changedRule); + } catch (error: unknown) { + next(error); + } + } + + static async createRuleset(req: Request, res: Response, next: NextFunction) { + try { + const { name, rulesetTypeId, carNumber, active, fileId } = req.body; + + const ruleset = await RulesService.createRuleset( + req.currentUser, + req.organization, + name, + rulesetTypeId, + carNumber, + active, + fileId + ); + + res.status(200).json(ruleset); + } catch (error: unknown) { + next(error); + } + } + + static async deleteRulesetType(req: Request, res: Response, next: NextFunction) { + try { + const { rulesetTypeId } = req.params; + + const rulesetType = await RulesService.deleteRulesetType(req.currentUser, rulesetTypeId, req.organization); + + res.status(200).json(rulesetType); + } catch (error: unknown) { + next(error); + } + } + + static async updateRuleset(req: Request, res: Response, next: NextFunction) { + try { + const { rulesetId } = req.params; + const { name, isActive } = req.body; + + const ruleset: Ruleset = await RulesService.updateRuleset( + req.currentUser, + req.organization.organizationId, + rulesetId, + name, + isActive + ); + + res.status(200).json(ruleset); + } catch (error: unknown) { + next(error); + } + } + + static async getChildRules(req: Request, res: Response, next: NextFunction) { + try { + const { ruleId: parentRuleId } = req.params; + const childrenRules: Rule[] = await RulesService.getChildRules(parentRuleId, req.organization); + + res.status(200).json(childrenRules); + } catch (error: unknown) { + next(error); + } + } + + static async getUnassignedRules(req: Request, res: Response, next: NextFunction) { + try { + const { rulesetId } = req.params; + const rules = await RulesService.getUnassignedRules(rulesetId, req.organization); + res.status(200).json(rules); + } catch (error: unknown) { + next(error); + } + } + + static async getUnassignedRulesForRuleset(req: Request, res: Response, next: NextFunction) { + try { + const { rulesetId, teamId } = req.params; + const rules = await RulesService.getUnassignedRulesForRuleset(rulesetId, teamId, req.organization.organizationId); + res.status(200).json(rules); + } catch (error: unknown) { + next(error); + } + } + + static async getProjectRules(req: Request, res: Response, next: NextFunction) { + try { + const { rulesetId, projectId } = req.params; + + const projectRules = await RulesService.getProjectRules(rulesetId, projectId, req.organization); + + res.status(200).json(projectRules); + } catch (error: unknown) { + next(error); + } + } + + static async getTeamRulesInRulesetType(req: Request, res: Response, next: NextFunction) { + try { + const { rulesetTypeId, teamId } = req.params; + const rules = await RulesService.getTeamRulesInRulesetType(teamId, rulesetTypeId, req.organization); + res.status(200).json(rules); + } catch (error: unknown) { + next(error); + } + } + + static async getTopLevelRules(req: Request, res: Response, next: NextFunction) { + try { + const { rulesetId } = req.params; + const rules = await RulesService.getTopLevelRules(rulesetId, req.organization.organizationId); + res.status(200).json(rules); + } catch (error: unknown) { + next(error); + } + } + + static async parseRuleset(req: Request, res: Response, next: NextFunction) { + try { + const { fileId, parserType } = req.body; + const { rulesetId } = req.params; + + const parseResult = await RulesService.parseRuleset( + req.currentUser, + req.organization.organizationId, + fileId, + rulesetId, + parserType + ); + + res.status(200).json(parseResult); + } catch (error: unknown) { + next(error); + } + } + + static async uploadRulesetFile(req: Request, res: Response, next: NextFunction) { + try { + if (!req.file) { + throw new HttpException(400, 'Invalid or undefined file data'); + } + + const fileId = await RulesService.uploadRulesetFile(req.file, req.currentUser, req.organization); + res.status(200).json(fileId); + } catch (error: unknown) { + next(error); + } + } + + static async getSingleRuleset(req: Request, res: Response, next: NextFunction) { + try { + const { rulesetId } = req.params; + const ruleset = await RulesService.getSingleRuleset(req.currentUser, rulesetId, req.organization); + res.status(200).json(ruleset); + } catch (error: unknown) { + next(error); + } + } +} diff --git a/src/backend/src/prisma-query-args/rules.query-args.ts b/src/backend/src/prisma-query-args/rules.query-args.ts new file mode 100644 index 0000000000..32a602408c --- /dev/null +++ b/src/backend/src/prisma-query-args/rules.query-args.ts @@ -0,0 +1,94 @@ +import { Prisma } from '@prisma/client'; + +export type RulePreviewQueryArgs = ReturnType; + +// preview for rule display +export const getRulePreviewQueryArgs = () => + Prisma.validator()({ + include: { + parentRule: { + select: { + ruleId: true, + ruleCode: true + } + }, + subRules: { + select: { + ruleId: true + } + }, + referencedRule: { + select: { + ruleId: true + } + }, + teams: { + select: { + teamId: true, + teamName: true + } + } + } + }); + +export const getProjectRuleQueryArgs = () => + Prisma.validator()({ + include: { + rule: getRulePreviewQueryArgs(), + project: { select: { projectId: true } }, + statusHistory: { + include: { + createdBy: { + select: { + userId: true, + firstName: true, + lastName: true + } + } + }, + orderBy: { + dateCreated: 'desc' + } + } + } + }); + +export type RulesetQueryArgs = ReturnType; + +export const getRulesetQueryArgs = () => + Prisma.validator()({ + include: { + rules: { + where: { dateDeleted: null }, + select: { + ruleId: true, + _count: { + select: { + teams: true + } + } + } + }, + rulesetType: true, + car: { + include: { + wbsElement: true + } + } + } + }); + +export const getRulesetPreviewQueryArgs = () => + Prisma.validator()({ + select: { + name: true, + dateCreated: true, + rulesetType: true, + active: true, + car: { + include: { + wbsElement: true + } + } + } + }); diff --git a/src/backend/src/prisma/migrations/20251111150202_rules_dashboard/migration.sql b/src/backend/src/prisma/migrations/20251111150202_rules_dashboard/migration.sql new file mode 100644 index 0000000000..c72bbb5c14 --- /dev/null +++ b/src/backend/src/prisma/migrations/20251111150202_rules_dashboard/migration.sql @@ -0,0 +1,181 @@ +-- CreateEnum +CREATE TYPE "public"."Rule_Completion" AS ENUM ('REVIEW', 'INCOMPLETE', 'COMPLETED'); + +-- CreateTable +CREATE TABLE "public"."Ruleset_Type" ( + "rulesetTypeId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "lastUpdated" TIMESTAMP(3) NOT NULL, + "dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdByUserId" TEXT NOT NULL, + "dateDeleted" TIMESTAMP(3), + "deletedByUserId" TEXT, + "organizationId" TEXT NOT NULL, + + CONSTRAINT "Ruleset_Type_pkey" PRIMARY KEY ("rulesetTypeId") +); + +-- CreateTable +CREATE TABLE "public"."Ruleset" ( + "rulesetId" TEXT NOT NULL, + "fileId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "active" BOOLEAN NOT NULL, + "rulesetTypeId" TEXT NOT NULL, + "carId" TEXT NOT NULL, + "dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdByUserId" TEXT NOT NULL, + "dateDeleted" TIMESTAMP(3), + "deletedByUserId" TEXT, + + CONSTRAINT "Ruleset_pkey" PRIMARY KEY ("rulesetId") +); + +-- CreateTable +CREATE TABLE "public"."Rule" ( + "ruleId" TEXT NOT NULL, + "ruleCode" TEXT NOT NULL, + "ruleContent" TEXT NOT NULL, + "imageFileIds" TEXT[], + "rulesetId" TEXT NOT NULL, + "parentRuleId" TEXT, + "dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "dateUpdated" TIMESTAMP(3), + "dateDeleted" TIMESTAMP(3), + "createdByUserId" TEXT NOT NULL, + "updatedByUserId" TEXT, + "deletedByUserId" TEXT, + + CONSTRAINT "Rule_pkey" PRIMARY KEY ("ruleId") +); + +-- CreateTable +CREATE TABLE "public"."Rule_Status_Change" ( + "historyId" TEXT NOT NULL, + "projectRuleId" TEXT NOT NULL, + "dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdByUserId" TEXT NOT NULL, + "dateDeleted" TIMESTAMP(3), + "deletedByUserId" TEXT, + "newStatus" "public"."Rule_Completion" NOT NULL, + "note" TEXT NOT NULL, + + CONSTRAINT "Rule_Status_Change_pkey" PRIMARY KEY ("historyId") +); + +-- CreateTable +CREATE TABLE "public"."Project_Rule" ( + "projectRuleId" TEXT NOT NULL, + "ruleId" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "currentStatus" "public"."Rule_Completion" NOT NULL, + "dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdByUserId" TEXT NOT NULL, + "dateDeleted" TIMESTAMP(3), + "deletedByUserId" TEXT, + + CONSTRAINT "Project_Rule_pkey" PRIMARY KEY ("projectRuleId") +); + +-- CreateTable +CREATE TABLE "public"."_ruleReferences" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_ruleReferences_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateTable +CREATE TABLE "public"."_teamRules" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_teamRules_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateIndex +CREATE INDEX "Ruleset_Type_organizationId_idx" ON "public"."Ruleset_Type"("organizationId"); + +-- CreateIndex +CREATE INDEX "Rule_parentRuleId_rulesetId_ruleCode_idx" ON "public"."Rule"("parentRuleId", "rulesetId", "ruleCode"); + +-- CreateIndex +CREATE UNIQUE INDEX "Rule_rulesetId_ruleCode_key" ON "public"."Rule"("rulesetId", "ruleCode"); + +-- CreateIndex +CREATE UNIQUE INDEX "Project_Rule_ruleId_projectId_key" ON "public"."Project_Rule"("ruleId", "projectId"); + +-- CreateIndex +CREATE INDEX "_ruleReferences_B_index" ON "public"."_ruleReferences"("B"); + +-- CreateIndex +CREATE INDEX "_teamRules_B_index" ON "public"."_teamRules"("B"); + +-- AddForeignKey +ALTER TABLE "public"."Ruleset_Type" ADD CONSTRAINT "Ruleset_Type_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "public"."User"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Ruleset_Type" ADD CONSTRAINT "Ruleset_Type_deletedByUserId_fkey" FOREIGN KEY ("deletedByUserId") REFERENCES "public"."User"("userId") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Ruleset_Type" ADD CONSTRAINT "Ruleset_Type_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "public"."Organization"("organizationId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Ruleset" ADD CONSTRAINT "Ruleset_rulesetTypeId_fkey" FOREIGN KEY ("rulesetTypeId") REFERENCES "public"."Ruleset_Type"("rulesetTypeId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Ruleset" ADD CONSTRAINT "Ruleset_carId_fkey" FOREIGN KEY ("carId") REFERENCES "public"."Car"("carId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Ruleset" ADD CONSTRAINT "Ruleset_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "public"."User"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Ruleset" ADD CONSTRAINT "Ruleset_deletedByUserId_fkey" FOREIGN KEY ("deletedByUserId") REFERENCES "public"."User"("userId") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Rule" ADD CONSTRAINT "Rule_rulesetId_fkey" FOREIGN KEY ("rulesetId") REFERENCES "public"."Ruleset"("rulesetId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Rule" ADD CONSTRAINT "Rule_parentRuleId_fkey" FOREIGN KEY ("parentRuleId") REFERENCES "public"."Rule"("ruleId") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Rule" ADD CONSTRAINT "Rule_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "public"."User"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Rule" ADD CONSTRAINT "Rule_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "public"."User"("userId") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Rule" ADD CONSTRAINT "Rule_deletedByUserId_fkey" FOREIGN KEY ("deletedByUserId") REFERENCES "public"."User"("userId") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Rule_Status_Change" ADD CONSTRAINT "Rule_Status_Change_projectRuleId_fkey" FOREIGN KEY ("projectRuleId") REFERENCES "public"."Project_Rule"("projectRuleId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Rule_Status_Change" ADD CONSTRAINT "Rule_Status_Change_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "public"."User"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Rule_Status_Change" ADD CONSTRAINT "Rule_Status_Change_deletedByUserId_fkey" FOREIGN KEY ("deletedByUserId") REFERENCES "public"."User"("userId") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Project_Rule" ADD CONSTRAINT "Project_Rule_ruleId_fkey" FOREIGN KEY ("ruleId") REFERENCES "public"."Rule"("ruleId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Project_Rule" ADD CONSTRAINT "Project_Rule_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "public"."Project"("projectId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Project_Rule" ADD CONSTRAINT "Project_Rule_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "public"."User"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Project_Rule" ADD CONSTRAINT "Project_Rule_deletedByUserId_fkey" FOREIGN KEY ("deletedByUserId") REFERENCES "public"."User"("userId") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."_ruleReferences" ADD CONSTRAINT "_ruleReferences_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Rule"("ruleId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."_ruleReferences" ADD CONSTRAINT "_ruleReferences_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."Rule"("ruleId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."_teamRules" ADD CONSTRAINT "_teamRules_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Rule"("ruleId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."_teamRules" ADD CONSTRAINT "_teamRules_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."Team"("teamId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/backend/src/prisma/schema.prisma b/src/backend/src/prisma/schema.prisma index dc3295fba5..fae471b8ac 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -153,6 +153,12 @@ enum Review_Status { APPROVED } +enum Rule_Completion { + REVIEW // all rules start as REVIEW + INCOMPLETE // rules that need an action + COMPLETED +} + model User { userId String @id @default(uuid()) firstName String @@ -261,6 +267,17 @@ model User { deletedSponsorTiers Sponsor_Tier[] financeDelegateForOrganizations Organization[] @relation(name: "financeDelegates") assignedReimbursementRequests Reimbursement_Request[] @relation(name: "reimbursementRequestAssignee") + createdRuleStatuses Rule_Status_Change[] @relation(name: "ruleStatusCreator") + deletedRuleStatuses Rule_Status_Change[] @relation(name: "ruleStatusDeletor") + createdRulesetTypes Ruleset_Type[] @relation(name: "rulesetTypeCreator") + deletedRulesetTypes Ruleset_Type[] @relation(name: "rulesetTypeDeleter") + createdRulesets Ruleset[] @relation(name: "rulesetCreator") + deletedRulesets Ruleset[] @relation(name: "rulesetDeleter") + createdRules Rule[] @relation(name: "ruleCreator") + updatedRules Rule[] @relation(name: "ruleUpdater") + deletedRules Rule[] @relation(name: "ruleDeletor") + createdProjectRules Project_Rule[] @relation(name: "projectRuleCreator") + deletedProjectRules Project_Rule[] @relation(name: "projectRuleDeletor") } model Role { @@ -297,6 +314,7 @@ model Team { organization Organization @relation(fields: [organizationId], references: [organizationId]) checklists Checklist[] projectTemplates Project_Template[] + rules Rule[] @relation(name: "teamRules") @@index([headId]) @@index([organizationId]) @@ -524,6 +542,7 @@ model Project { featuredByOrganization Organization? @relation(fields: [featuredByOrganizationId], references: [organizationId]) abbreviation String? parts Part[] + rules Project_Rule[] @relation(name: "projectsForRule") @@index([carId]) } @@ -1150,6 +1169,7 @@ model Car { wbsElementId String @unique wbsElement WBS_Element @relation(fields: [wbsElementId], references: [wbsElementId]) linkedGraphs Graph[] @relation(name: "graphCars") + rulesets Ruleset[] @relation(name: "rulesetCar") @@index([wbsElementId]) } @@ -1212,6 +1232,7 @@ model Organization { sponsorTiers Sponsor_Tier[] indexCodes Index_Code[] financeDelegates User[] @relation(name: "financeDelegates") + rulesetTypes Ruleset_Type[] } model FrequentlyAskedQuestion { @@ -1548,3 +1569,98 @@ model Reimbursement_Request_Comment { @@index([reimbursementRequestId]) } + +model Ruleset_Type { + rulesetTypeId String @id @default(uuid()) + name String + lastUpdated DateTime @updatedAt + revisionFiles Ruleset[] @relation(name: "rulesetFileType") + dateCreated DateTime @default(now()) + createdByUserId String + createdBy User @relation(name: "rulesetTypeCreator", fields: [createdByUserId], references: [userId]) + dateDeleted DateTime? + deletedByUserId String? + deletedBy User? @relation(name: "rulesetTypeDeleter", fields: [deletedByUserId], references: [userId]) + organizationId String + organization Organization @relation(fields: [organizationId], references: [organizationId]) + + @@index([organizationId]) +} + +model Ruleset { + rulesetId String @id @default(uuid()) + fileId String + name String + active Boolean + rules Rule[] + rulesetTypeId String + rulesetType Ruleset_Type @relation(name: "rulesetFileType", fields: [rulesetTypeId], references: [rulesetTypeId]) + carId String + car Car @relation(name: "rulesetCar", fields: [carId], references: [carId]) + dateCreated DateTime @default(now()) + createdByUserId String + createdBy User @relation(name: "rulesetCreator", fields: [createdByUserId], references: [userId]) + dateDeleted DateTime? + deletedByUserId String? + deletedBy User? @relation(name: "rulesetDeleter", fields: [deletedByUserId], references: [userId]) +} + +model Rule { + ruleId String @id @default(uuid()) + ruleCode String + ruleContent String + imageFileIds String[] + rulesetId String + ruleset Ruleset @relation(fields: [rulesetId], references: [rulesetId]) + parentRuleId String? + parentRule Rule? @relation(name: "subRules", fields: [parentRuleId], references: [ruleId]) + subRules Rule[] @relation(name: "subRules") + referencedRule Rule[] @relation(name: "ruleReferences") + referencedBy Rule[] @relation(name: "ruleReferences") + projects Project_Rule[] @relation(name: "rulesInProject") + teams Team[] @relation(name: "teamRules") + dateCreated DateTime @default(now()) + dateUpdated DateTime? @updatedAt + dateDeleted DateTime? + createdByUserId String + createdBy User @relation(name: "ruleCreator", fields: [createdByUserId], references: [userId]) + updatedByUserId String? + updatedBy User? @relation(name: "ruleUpdater", fields: [updatedByUserId], references: [userId]) + deletedByUserId String? + deletedBy User? @relation(name: "ruleDeletor", fields: [deletedByUserId], references: [userId]) + + @@unique([rulesetId, ruleCode]) + @@index([parentRuleId, rulesetId, ruleCode]) +} + +model Rule_Status_Change { + historyId String @id @default(uuid()) + projectRule Project_Rule @relation(name: "ruleStatusHistory", fields: [projectRuleId], references: [projectRuleId]) + projectRuleId String + dateCreated DateTime @default(now()) + createdByUserId String + createdBy User @relation(name: "ruleStatusCreator", fields: [createdByUserId], references: [userId]) + dateDeleted DateTime? + deletedByUserId String? + deletedBy User? @relation(name: "ruleStatusDeletor", fields: [deletedByUserId], references: [userId]) + newStatus Rule_Completion + note String +} + +model Project_Rule { + projectRuleId String @id @default(uuid()) + ruleId String + rule Rule @relation(name: "rulesInProject", fields: [ruleId], references: [ruleId]) + projectId String + project Project @relation(name: "projectsForRule", fields: [projectId], references: [projectId]) + currentStatus Rule_Completion + statusHistory Rule_Status_Change[] @relation(name: "ruleStatusHistory") + dateCreated DateTime @default(now()) + createdByUserId String + createdBy User @relation(name: "projectRuleCreator", fields: [createdByUserId], references: [userId]) + dateDeleted DateTime? + deletedByUserId String? + deletedBy User? @relation(name: "projectRuleDeletor", fields: [deletedByUserId], references: [userId]) + + @@unique([ruleId, projectId]) +} diff --git a/src/backend/src/prisma/seed-data/rules.seed.ts b/src/backend/src/prisma/seed-data/rules.seed.ts new file mode 100644 index 0000000000..7542302fc5 --- /dev/null +++ b/src/backend/src/prisma/seed-data/rules.seed.ts @@ -0,0 +1,141 @@ +import type { Prisma } from '@prisma/client'; +import { Organization } from '@prisma/client'; +import RulesService from '../../services/rules.services'; +import { User } from 'shared'; + +// rules +const topLevelRule = (rulesetId: string, userCreatedId: string): Prisma.RuleCreateInput => { + return { + ruleCode: 'T', + ruleContent: 'PART T - GENERAL TECHNICAL REQUIREMENTS', + imageFileIds: [], + dateCreated: new Date('2025-09-01T10:00:00Z'), + ruleset: { connect: { rulesetId } }, + createdBy: { connect: { userId: userCreatedId } } + }; +}; + +const secondLevelRule = (rulesetId: string, userCreatedId: string, parentRuleId: string): Prisma.RuleCreateInput => { + return { + ruleCode: 'T2', + ruleContent: 'ARTICLE T2 GENERAL DESIGN REQUIREMENTS', + imageFileIds: [], + dateCreated: new Date('2025-09-01T10:00:00Z'), + ruleset: { connect: { rulesetId } }, + createdBy: { connect: { userId: userCreatedId } }, + parentRule: { connect: { ruleId: parentRuleId } } + }; +}; + +const thirdLevelRule = (rulesetId: string, userCreatedId: string, parentRuleId: string): Prisma.RuleCreateInput => { + return { + ruleCode: 'T2.1', + ruleContent: 'T2.1 Vehicle Configuration', + imageFileIds: [], + dateCreated: new Date('2025-09-01T10:00:00Z'), + ruleset: { connect: { rulesetId } }, + createdBy: { connect: { userId: userCreatedId } }, + parentRule: { connect: { ruleId: parentRuleId } } + }; +}; + +const leafRule = (rulesetId: string, userCreatedId: string, parentRuleId: string): Prisma.RuleCreateInput => { + return { + ruleCode: 'T2.1.1', + ruleContent: + 'The vehicle must be open-wheeled and open-cockpit (a formula style body) with four (4) wheels that are not in a straight line.', + imageFileIds: [], + dateCreated: new Date('2025-09-01T10:00:00Z'), + ruleset: { connect: { rulesetId } }, + createdBy: { connect: { userId: userCreatedId } }, + parentRule: { connect: { ruleId: parentRuleId } } + }; +}; + +// ruleset types +const rulesetType1 = (userCreatedId: string, organizationId: string): Prisma.Ruleset_TypeCreateInput => { + return { + name: 'FSAE', + createdBy: { connect: { userId: userCreatedId } }, + organization: { connect: { organizationId } } + }; +}; + +const rulesetType2 = (userCreatedId: string, organizationId: string): Prisma.Ruleset_TypeCreateInput => { + return { + name: 'FHE', + createdBy: { connect: { userId: userCreatedId } }, + organization: { connect: { organizationId } } + }; +}; + +const emptyRulesetType = (userCreatedId: string, organizationId: string): Prisma.Ruleset_TypeCreateInput => { + return { + name: 'Empty Ruleset Type', + createdBy: { connect: { userId: userCreatedId } }, + organization: { connect: { organizationId } } + }; +}; + +// rulesets +const ruleset1 = (carId: string, userCreatedId: string, rulesetTypeId: string): Prisma.RulesetCreateInput => { + return { + name: 'FSAE Rules 2025', + fileId: 'fsae-rules-2025', + active: true, + dateCreated: new Date('2025-01-01T10:00:00Z'), + car: { connect: { carId } }, + createdBy: { connect: { userId: userCreatedId } }, + rulesetType: { connect: { rulesetTypeId } } + }; +}; + +const secondActiveRuleset = (carId: string, userCreatedId: string, rulesetTypeId: string): Prisma.RulesetCreateInput => { + return { + name: 'Another Active FSAE Rules 2025 Revision', + fileId: '2active-fsae-rules-2025', + active: true, + dateCreated: new Date('2024-12-31T10:00:00Z'), + car: { connect: { carId } }, + createdBy: { connect: { userId: userCreatedId } }, + rulesetType: { connect: { rulesetTypeId } } + }; +}; + +// project rules +const projectRule1 = (projectId: string, ruleId: string, createdByUserId: string): Prisma.Project_RuleCreateInput => { + return { + currentStatus: 'REVIEW', + rule: { connect: { ruleId } }, + project: { connect: { projectId } }, + createdBy: { connect: { userId: createdByUserId } } + }; +}; + +const projectRule2 = (projectId: string, ruleId: string, createdByUserId: string): Prisma.Project_RuleCreateInput => { + return { + currentStatus: 'REVIEW', + rule: { connect: { ruleId } }, + project: { connect: { projectId } }, + createdBy: { connect: { userId: createdByUserId } } + }; +}; + +export const seedRulesetType = async (submitter: User, name: string, organization: Organization) => { + const createdRulesetType = await RulesService.createRulesetType(submitter, name, organization); + return createdRulesetType; +}; + +export const ruleSeedData = { + topLevelRule, + secondLevelRule, + thirdLevelRule, + leafRule, + rulesetType1, + rulesetType2, + emptyRulesetType, + ruleset1, + secondActiveRuleset, + projectRule1, + projectRule2 +}; diff --git a/src/backend/src/prisma/seed.ts b/src/backend/src/prisma/seed.ts index 95f880bf30..5ae34aa9f0 100644 --- a/src/backend/src/prisma/seed.ts +++ b/src/backend/src/prisma/seed.ts @@ -19,7 +19,6 @@ import { } from '@prisma/client'; import { createUser, dbSeedAllUsers } from './seed-data/users.seed'; import { dbSeedAllTeams } from './seed-data/teams.seed'; -import { seedReimbursementRequests } from './seed-data/reimbursement-requests.seed'; import ChangeRequestsService from '../services/change-requests.services'; import TeamsService from '../services/teams.services'; import { @@ -28,7 +27,6 @@ import { RoleEnum, SpecialPermission, StandardChangeRequest, - User, WbsElementStatus, WorkPackageStage } from 'shared'; @@ -46,10 +44,14 @@ import { writeFileSync } from 'fs'; import WbsElementTemplatesService from '../services/wbs-element-templates.services'; import RecruitmentServices from '../services/recruitment.services'; import OrganizationsService from '../services/organizations.services'; +import { seedGraph } from './seed-data/statistics.seed'; import AnnouncementService from '../services/announcement.services'; import OnboardingServices from '../services/onboarding.services'; import { dbSeedAllParts, dbSeedAllPartTags } from './seed-data/parts.seed'; import FinanceServices from '../services/finance.services'; +import { ruleSeedData } from './seed-data/rules.seed'; +import RulesService from '../services/rules.services'; +import { seedRulesetType } from './seed-data/rules.seed'; const prisma = new PrismaClient(); @@ -272,7 +274,7 @@ const performSeed: () => Promise = async () => { const trang = await createUser(dbSeedAllUsers.trang, RoleEnum.MEMBER, organizationId); const regina = await createUser(dbSeedAllUsers.regina, RoleEnum.MEMBER, organizationId); const patrick = await createUser(dbSeedAllUsers.patrick, RoleEnum.MEMBER, organizationId); - const spongebob = await createUser(dbSeedAllUsers.spongebob, RoleEnum.MEMBER, organizationId); + const spongebob = await createUser(dbSeedAllUsers.spongebob, RoleEnum.GUEST, organizationId); await UsersService.updateUserRole(cyborg.userId, thomasEmrax, 'APP_ADMIN', ner); @@ -426,14 +428,6 @@ const performSeed: () => Promise = async () => { ner ); - // Set finance delegates for the organization - await OrganizationsService.setFinanceDelegates(thomasEmrax, organizationId, [ - monopolyMan.userId, - mrKrabs.userId, - richieRich.userId, - johnBoddy.userId - ]); - await TeamsService.setTeamMembers( aang, avatarBenders.teamId, @@ -1121,8 +1115,8 @@ const performSeed: () => Promise = async () => { 'Bodywork Concept of Design', changeRequestProject1Id, WorkPackageStage.Design, - weeksAgo(12).toISOString().split('T')[0], - 6, + '01/01/2023', + 3, [], [], thomasEmrax, @@ -1141,7 +1135,7 @@ const performSeed: () => Promise = async () => { 'ACTIVATION', thomasEmrax.userId, joeShmoe.userId, - weeksAgo(12), + new Date('2024-03-25T04:00:00.000Z'), true, ner ); @@ -1167,7 +1161,7 @@ const performSeed: () => Promise = async () => { 'Adhesive Shear Strength Test', changeRequestProject1Id, WorkPackageStage.Research, - weeksAgo(10).toISOString().split('T')[0], + '01/22/2023', 5, [], [], @@ -1185,8 +1179,8 @@ const performSeed: () => Promise = async () => { 'Manufacture Wiring Harness', changeRequestProject5Id, WorkPackageStage.Manufacturing, - weeksAgo(9).toISOString().split('T')[0], - 4, + '02/01/2023', + 3, [], [], thomasEmrax, @@ -1205,7 +1199,7 @@ const performSeed: () => Promise = async () => { CR_Type.ACTIVATION, regina.userId, janis.userId, - weeksAgo(9), + new Date('2023-08-21T04:00:00.000Z'), true, ner ); @@ -1218,8 +1212,8 @@ const performSeed: () => Promise = async () => { 'Install Wiring Harness', changeRequestProject5Id, WorkPackageStage.Install, - weeksAgo(5).toISOString().split('T')[0], - 6, + '04/01/2023', + 7, [], [], thomasEmrax, @@ -1238,7 +1232,7 @@ const performSeed: () => Promise = async () => { CR_Type.ACTIVATION, joeShmoe.userId, thomasEmrax.userId, - weeksAgo(5), + new Date('2023-10-02T04:00:00.000Z'), true, ner ); @@ -1251,7 +1245,7 @@ const performSeed: () => Promise = async () => { 'Design Plush', changeRequestProject6Id, WorkPackageStage.Design, - weeksAgo(16).toISOString().split('T')[0], + '04/02/2023', 7, [], [], @@ -1271,7 +1265,7 @@ const performSeed: () => Promise = async () => { CR_Type.ACTIVATION, katara.userId, aang.userId, - weeksAgo(16), + new Date('2023-05-08T04:00:00.000Z'), true, ner ); @@ -1284,8 +1278,8 @@ const performSeed: () => Promise = async () => { 'Put Plush Together', changeRequestProject6Id, WorkPackageStage.Manufacturing, - weeksAgo(9).toISOString().split('T')[0], - 5, + '04/02/2023', + 7, [], [], aang, @@ -1304,7 +1298,7 @@ const performSeed: () => Promise = async () => { CR_Type.ACTIVATION, katara.userId, aang.userId, - weeksAgo(9), + new Date('2023-07-31T04:00:00.000Z'), true, ner ); @@ -1317,8 +1311,8 @@ const performSeed: () => Promise = async () => { 'Plush Testing', changeRequestProject6Id, WorkPackageStage.Testing, - weeksAgo(4).toISOString().split('T')[0], - 4, + '04/02/2023', + 3, [], [], aang, @@ -1337,7 +1331,7 @@ const performSeed: () => Promise = async () => { CR_Type.ACTIVATION, katara.userId, aang.userId, - weeksAgo(4), + new Date('2023-10-09T04:00:00.000Z'), true, ner ); @@ -1351,8 +1345,8 @@ const performSeed: () => Promise = async () => { 'Design Laser Canon', changeRequestProject7Id, WorkPackageStage.Design, - weeksAgo(8).toISOString().split('T')[0], - 5, + '01/01/2023', + 3, [], [], zatanna, @@ -1371,7 +1365,7 @@ const performSeed: () => Promise = async () => { CR_Type.ACTIVATION, zatanna.userId, lexLuther.userId, - weeksAgo(8), + new Date('2024-03-25T04:00:00.000Z'), true, ner ); @@ -1384,8 +1378,8 @@ const performSeed: () => Promise = async () => { 'Laser Canon Research', changeRequestProject7Id, WorkPackageStage.Research, - weeksAgo(3).toISOString().split('T')[0], - 6, + '01/22/2023', + 5, [], [], zatanna, @@ -1402,8 +1396,8 @@ const performSeed: () => Promise = async () => { 'Laser Canon Testing', changeRequestProject7Id, WorkPackageStage.Testing, - weeksFromNow(3).toISOString().split('T')[0], - 4, + '02/15/2023', + 3, [], [], zatanna, @@ -1421,8 +1415,8 @@ const performSeed: () => Promise = async () => { 'Stadium Research', changeRequestProject8Id, WorkPackageStage.Research, - weeksAgo(14).toISOString().split('T')[0], - 7, + '02/01/2023', + 5, [], [], mikeMacdonald, @@ -1441,7 +1435,7 @@ const performSeed: () => Promise = async () => { CR_Type.ACTIVATION, mikeMacdonald.userId, ryanGiggs.userId, - weeksAgo(14), + new Date('2023-08-21T04:00:00.000Z'), true, ner ); @@ -1454,8 +1448,8 @@ const performSeed: () => Promise = async () => { 'Stadium Install', changeRequestProject8Id, WorkPackageStage.Install, - weeksAgo(7).toISOString().split('T')[0], - 6, + '03/01/2023', + 8, [], [], mikeMacdonald, @@ -1472,8 +1466,8 @@ const performSeed: () => Promise = async () => { 'Stadium Testing', changeRequestProject8Id, WorkPackageStage.Testing, - weeksAgo(1).toISOString().split('T')[0], - 5, + '06/01/2023', + 3, [], [], mikeMacdonald, @@ -1536,7 +1530,7 @@ const performSeed: () => Promise = async () => { CR_Type.ACTIVATION, thomasEmrax.userId, joeShmoe.userId, - weeksAgo(9), + new Date('02/01/2023'), true, ner ); @@ -1816,7 +1810,7 @@ const performSeed: () => Promise = async () => { /** * Reimbursements */ - const vendorTesla = await ReimbursementRequestService.createVendor( + const vendor = await ReimbursementRequestService.createVendor( thomasEmrax, 'Tesla', ner, @@ -1827,7 +1821,7 @@ const performSeed: () => Promise = async () => { 'racecar228!', 'SAVE50!' ); - const vendorAmazon = await ReimbursementRequestService.createVendor( + await ReimbursementRequestService.createVendor( thomasEmrax, 'Amazon', ner, @@ -1838,7 +1832,7 @@ const performSeed: () => Promise = async () => { 'racecare228!', 'SAVE20!' ); - const vendorGoogle = await ReimbursementRequestService.createVendor( + await ReimbursementRequestService.createVendor( thomasEmrax, 'Google', ner, @@ -1849,7 +1843,7 @@ const performSeed: () => Promise = async () => { 'racecar228!', 'SAVE50!' ); - const vendorMicrosoft = await ReimbursementRequestService.createVendor( + await ReimbursementRequestService.createVendor( thomasEmrax, 'Microsoft', ner, @@ -1860,7 +1854,7 @@ const performSeed: () => Promise = async () => { 'secure123!', 'WELCOME10' ); - const vendorApple = await ReimbursementRequestService.createVendor( + await ReimbursementRequestService.createVendor( thomasEmrax, 'Apple', ner, @@ -1871,7 +1865,7 @@ const performSeed: () => Promise = async () => { 'appl3Secure!', 'APPLE30' ); - const vendorCostco = await ReimbursementRequestService.createVendor( + await ReimbursementRequestService.createVendor( thomasEmrax, 'Costco', ner, @@ -1882,7 +1876,7 @@ const performSeed: () => Promise = async () => { 'bulkBuy22!', 'BULKDEAL' ); - const vendorWalmart = await ReimbursementRequestService.createVendor( + await ReimbursementRequestService.createVendor( thomasEmrax, 'Walmart', ner, @@ -1893,7 +1887,7 @@ const performSeed: () => Promise = async () => { 'WalMartP@ss1', 'ROLLBACK15' ); - const vendorTarget = await ReimbursementRequestService.createVendor( + await ReimbursementRequestService.createVendor( thomasEmrax, 'Target', ner, @@ -2103,109 +2097,122 @@ const performSeed: () => Promise = async () => { 3010 ); - // Add userSecureSettings for users who will create reimbursement requests - const usersNeedingSecureSettings = [ - { user: joeShmoe, varName: 'joeShmoe' }, - { user: batman, varName: 'batman' }, - { user: superman, varName: 'superman' }, - { user: flash, varName: 'flash' }, - { user: aquaman, varName: 'aquaman' }, - { user: wonderwoman, varName: 'wonderwoman' }, - { user: greenLantern, varName: 'greenLantern' }, - { user: cyborg, varName: 'cyborg' }, - { user: martianManhunter, varName: 'martianManhunter' }, - { user: nightwing, varName: 'nightwing' }, - { user: aang, varName: 'aang' }, - { user: katara, varName: 'katara' }, - { user: sokka, varName: 'sokka' }, - { user: toph, varName: 'toph' }, - { user: zuko, varName: 'zuko' }, - { user: regina, varName: 'regina' }, - { user: cady, varName: 'cady' }, - { user: gretchen, varName: 'gretchen' }, - { user: karen, varName: 'karen' }, - { user: spongebob, varName: 'spongebob' }, - { user: patrick, varName: 'patrick' } - ]; - - const updatedUsers: any = {}; - - for (let i = 0; i < usersNeedingSecureSettings.length; i++) { - const { user, varName } = usersNeedingSecureSettings[i]; - await prisma.user_Secure_Settings.create({ - data: { - userSecureSettingsId: `secure-${user.userId}`, - userId: user.userId, - nuid: `00123456${i.toString().padStart(2, '0')}`, - phoneNumber: `123456${i.toString().padStart(4, '0')}`, - street: `${100 + i} Main St`, - city: 'Boston', - state: 'MA', - zipcode: '02115' + const reimbursement1 = await ReimbursementRequestService.createReimbursementRequest( + thomasEmrax, + vendor.vendorId, + indexCodeCash.indexCodeId, + [], + [ + { + name: 'GLUE', + reason: { + carNumber: 0, + projectNumber: 1, + workPackageNumber: 0 + }, + cost: 200000, + refundSources: [ + { + indexCode: indexCodeBudget, + amount: 150000 + }, + { + indexCode: indexCodeCash, + amount: 50000 + } + ] } - }); - - // Re-fetch user with secure settings - const updatedUser = await prisma.user.findUnique({ - where: { userId: user.userId }, - include: { userSettings: true, userSecureSettings: true, roles: true } - }); - updatedUsers[varName] = updatedUser; - } - - // Seed comprehensive reimbursement requests with various statuses and assignees - const seededReimbursementRequests = await seedReimbursementRequests( - { - thomasEmrax, - joeShmoe: updatedUsers.joeShmoe, - batman: updatedUsers.batman, - superman: updatedUsers.superman, - flash: updatedUsers.flash, - aquaman: updatedUsers.aquaman, - wonderwoman: updatedUsers.wonderwoman, - greenLantern: updatedUsers.greenLantern, - cyborg: updatedUsers.cyborg, - martianManhunter: updatedUsers.martianManhunter, - robin: updatedUsers.nightwing, // Using nightwing as robin since robin wasn't stored in a variable - nightwing: updatedUsers.nightwing, - aang: updatedUsers.aang, - katara: updatedUsers.katara, - sokka: updatedUsers.sokka, - toph: updatedUsers.toph, - zuko: updatedUsers.zuko, - monopolyMan, - mrKrabs, - richieRich, - johnBoddy, - regina: updatedUsers.regina, - cady: updatedUsers.cady, - gretchen: updatedUsers.gretchen, - karen: updatedUsers.karen, - spongebob: updatedUsers.spongebob, - patrick: updatedUsers.patrick - }, - { - tesla: vendorTesla, - amazon: vendorAmazon, - google: vendorGoogle, - microsoft: vendorMicrosoft, - apple: vendorApple, - costco: vendorCostco, - walmart: vendorWalmart, - target: vendorTarget - }, - { - cash: indexCodeCash, - budget: indexCodeBudget - }, - { - equipment: accountCode, - things: accountCode2, - stuff: accountCode3 - }, + ], + accountCode.accountCodeId, + 100, ner ); + const reimbursement3 = await ReimbursementRequestService.createReimbursementRequest( + thomasEmrax, + vendor.vendorId, + indexCodeBudget.indexCodeId, + [], + [ + { + name: 'BOX', + reason: { + carNumber: 0, + projectNumber: 1, + workPackageNumber: 0 + }, + cost: 200000, + refundSources: [ + { + indexCode: indexCodeBudget, + amount: 150000 + }, + { + indexCode: indexCodeCash, + amount: 50000 + } + ] + } + ], + accountCode.accountCodeId, + 200, + ner, + new Date() + ); + + const reimbursement2 = await ReimbursementRequestService.createReimbursementRequest( + thomasEmrax, + vendor.vendorId, + indexCodeBudget.indexCodeId, + [], + [ + { + name: 'BOX', + reason: { + carNumber: 0, + projectNumber: 1, + workPackageNumber: 0 + }, + cost: 10000, + refundSources: [ + { + indexCode: indexCodeBudget, + amount: 7000 + }, + { + indexCode: indexCodeCash, + amount: 3000 + } + ] + } + ], + accountCode.accountCodeId, + 20000, + ner, + new Date() + ); + + ReimbursementRequestService.createReimbursementRequestComment( + thomasEmrax, + ner, + 'Thomas Followed up - "Please upload reciept"', + reimbursement1.reimbursementRequestId + ); + + ReimbursementRequestService.createReimbursementRequestComment( + batman, + ner, + 'Batman Uploaded Receipt', + reimbursement1.reimbursementRequestId + ); + + ReimbursementRequestService.createReimbursementRequestComment( + thomasEmrax, + ner, + 'Thomas Submmited to SABO', + reimbursement1.reimbursementRequestId + ); + const otherProductReasonConsumables = await ReimbursementRequestService.createOtherReasonReimbursementProduct( 'CONSUMABLES', 10, @@ -2337,7 +2344,7 @@ const performSeed: () => Promise = async () => { undefined, undefined, undefined, - seededReimbursementRequests[0]?.reimbursementRequestId + reimbursement1.reimbursementRequestId ); // Need to do this because the design review cannot be scheduled for a past day @@ -2407,7 +2414,7 @@ const performSeed: () => Promise = async () => { 'Slim and Light Car', newWorkPackageChangeRequest.crId, WorkPackageStage.Design, - weeksAgo(2).toISOString().split('T')[0], + '01/22/2024', 5, [], [], @@ -2529,10 +2536,10 @@ const performSeed: () => Promise = async () => { { userId: regina.userId, title: 'Chief Electrical Engineer' } ]); - await RecruitmentServices.createMilestone(batman, 'Club fair!', 'Also meet us at:', daysAgo(120), ner); - await RecruitmentServices.createMilestone(batman, 'Applications Open', '', daysAgo(70), ner); - await RecruitmentServices.createMilestone(batman, 'Applications Close', '', daysAgo(56), ner); - await RecruitmentServices.createMilestone(batman, 'Decision Day!', '', daysAgo(49), ner); + await RecruitmentServices.createMilestone(batman, 'Club fair!', 'Also meet us at:', new Date('9/3/24'), ner); + await RecruitmentServices.createMilestone(batman, 'Applications Open', '', new Date('11/13/24'), ner); + await RecruitmentServices.createMilestone(batman, 'Applications Close', '', new Date('11/27/24'), ner); + await RecruitmentServices.createMilestone(batman, 'Decision Day!', '', new Date('12/4/24'), ner); await RecruitmentServices.createOrganizationFaq(batman, 'Who is the Chief Software Engineer?', 'Peyton McKee', ner); await RecruitmentServices.createOrganizationFaq( @@ -3062,7 +3069,7 @@ const performSeed: () => Promise = async () => { 'Google', true, 5000, - daysAgo(90), + new Date(12, 1, 24), [2024, 2025], goldSponsorTier.sponsorTierId, true, @@ -3075,12 +3082,404 @@ const performSeed: () => Promise = async () => { await FinanceServices.createSponsorTask( thomasEmrax, ner, - daysFromNow(30), + new Date(12, 1, 25), 'notes...', sponsor.sponsorId, - daysAgo(60), + new Date(7, 5, 25), thomasEmrax.userId ); + + /** + * Rules + */ + + // ruleset types + const fsaeRulesetType = await prisma.ruleset_Type.create({ + data: ruleSeedData.rulesetType1(batman.userId, ner.organizationId) + }); + + await prisma.ruleset_Type.create({ + data: ruleSeedData.rulesetType2(batman.userId, ner.organizationId) + }); + + await prisma.ruleset_Type.create({ + data: ruleSeedData.emptyRulesetType(batman.userId, ner.organizationId) + }); + + // rulesets + const ruleset1 = await prisma.ruleset.create({ + data: ruleSeedData.ruleset1(fergus.carId, batman.userId, fsaeRulesetType.rulesetTypeId) + }); + + await prisma.ruleset.create({ + data: ruleSeedData.secondActiveRuleset(fergus.carId, batman.userId, fsaeRulesetType.rulesetTypeId) + }); + + const fsae2025Ruleset = await prisma.ruleset.create({ + data: { + fileId: 'fsae-2025-rules-file-id', + name: '2025 FSAE Electric Rules', + active: true, + rulesetTypeId: fsaeRulesetType.rulesetTypeId, + carId: fergus.carId, + createdByUserId: batman.userId + } + }); + + const fsae2024Ruleset = await prisma.ruleset.create({ + data: { + fileId: 'fsae-2024-rules-file-id', + name: '2024 FSAE Electric Rules', + active: false, + rulesetTypeId: fsaeRulesetType.rulesetTypeId, + carId: fergus.carId, + createdByUserId: batman.userId + } + }); + + // rules + const ruleT = await prisma.rule.create({ data: ruleSeedData.topLevelRule(ruleset1.rulesetId, batman.userId) }); + const ruleT2 = await prisma.rule.create({ + data: ruleSeedData.secondLevelRule(ruleset1.rulesetId, batman.userId, ruleT.ruleId) + }); + const ruleT21 = await prisma.rule.create({ + data: ruleSeedData.thirdLevelRule(ruleset1.rulesetId, batman.userId, ruleT2.ruleId) + }); + const ruleT211 = await prisma.rule.create({ + data: ruleSeedData.leafRule(ruleset1.rulesetId, batman.userId, ruleT21.ruleId) + }); + + // project rules + await RulesService.createProjectRule(batman, ner, ruleT211.ruleId, project1Id); + + // Technical Rules Section + const techRule = await prisma.rule.create({ + data: { + ruleCode: 'T.1', + ruleContent: 'Technical Rules - All technical requirements for the vehicle must be met to compete', + rulesetId: fsae2025Ruleset.rulesetId, + createdByUserId: batman.userId + } + }); + + const vehicleConfigRule = await prisma.rule.create({ + data: { + ruleCode: 'T.1.1', + ruleContent: 'Vehicle Configuration - The vehicle must be a four-wheeled, open-wheel, open-cockpit vehicle', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: techRule.ruleId, + createdByUserId: thomasEmrax.userId, + imageFileIds: [] + } + }); + + const wheelRule = await prisma.rule.create({ + data: { + ruleCode: 'T.1.1.1', + ruleContent: 'All four wheels must be visible when viewed from above. Wheels must not exceed 13 inches in diameter', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: vehicleConfigRule.ruleId, + createdByUserId: joeShmoe.userId + } + }); + + const wheelbaseRule = await prisma.rule.create({ + data: { + ruleCode: 'T.1.1.2', + ruleContent: 'The wheelbase must be at least 1525 mm (60 inches)', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: vehicleConfigRule.ruleId, + createdByUserId: joeShmoe.userId + } + }); + + const trackWidthRule = await prisma.rule.create({ + data: { + ruleCode: 'T.1.1.3', + ruleContent: 'The smaller track width must be no less than 75% of the wheelbase', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: vehicleConfigRule.ruleId, + createdByUserId: thomasEmrax.userId + } + }); + + // Powertrain Rules + const powertrainRule = await prisma.rule.create({ + data: { + ruleCode: 'T.1.2', + ruleContent: 'Powertrain - Electric powertrain systems must comply with all electrical safety requirements', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: techRule.ruleId, + createdByUserId: thomasEmrax.userId + } + }); + + const motorRule = await prisma.rule.create({ + data: { + ruleCode: 'T.1.2.1', + ruleContent: 'The maximum nominal voltage of the accumulator must not exceed 600 VDC', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: powertrainRule.ruleId, + createdByUserId: joeShmoe.userId + } + }); + + const motorPowerRule = await prisma.rule.create({ + data: { + ruleCode: 'T.1.2.2', + ruleContent: 'The maximum continuous power delivered by the accumulator must not exceed 80 kW', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: powertrainRule.ruleId, + createdByUserId: joeBlow.userId + } + }); + + // Chassis Rules + const chassisRule = await prisma.rule.create({ + data: { + ruleCode: 'T.1.3', + ruleContent: 'Chassis and Frame - The chassis must provide adequate driver protection', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: techRule.ruleId, + createdByUserId: batman.userId, + imageFileIds: ['chassis-spec-drawing-1', 'chassis-spec-drawing-2'] + } + }); + + const chassisMaterialRule = await prisma.rule.create({ + data: { + ruleCode: 'T.1.3.1', + ruleContent: 'The frame must be a space frame design or a carbon fiber monocoque meeting specific standards', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: chassisRule.ruleId, + createdByUserId: thomasEmrax.userId + } + }); + + // Safety Rules Section + const safetyRule = await prisma.rule.create({ + data: { + ruleCode: 'S.1', + ruleContent: 'Safety Rules - All safety requirements must be met before the vehicle is allowed to compete', + rulesetId: fsae2025Ruleset.rulesetId, + createdByUserId: batman.userId + } + }); + + const frameRule = await prisma.rule.create({ + data: { + ruleCode: 'S.1.1', + ruleContent: + 'Frame Requirements - The main hoop must be directly behind the driver and be the tallest part of the car', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: safetyRule.ruleId, + createdByUserId: batman.userId + } + }); + + const rollHoopRule = await prisma.rule.create({ + data: { + ruleCode: 'S.1.1.1', + ruleContent: 'The main roll hoop must extend from the lowest chassis frame members on one side to the other', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: frameRule.ruleId, + createdByUserId: superman.userId + } + }); + + const harnessRule = await prisma.rule.create({ + data: { + ruleCode: 'S.1.2', + ruleContent: 'Harness - A 5-point or 6-point harness must be used, meeting SFI 16.1 or FIA 8853/98 standards', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: safetyRule.ruleId, + createdByUserId: superman.userId + } + }); + + const fireExtinguisherRule = await prisma.rule.create({ + data: { + ruleCode: 'S.1.3', + ruleContent: 'Fire Extinguisher - An onboard fire extinguisher system must be installed and accessible', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: safetyRule.ruleId, + createdByUserId: batman.userId + } + }); + + // Braking System Rules with Cross-References + const brakingRule = await prisma.rule.create({ + data: { + ruleCode: 'T.2.1', + ruleContent: + 'Braking System - The vehicle must have a braking system that acts on all four wheels and operates on two independent hydraulic circuits', + rulesetId: fsae2025Ruleset.rulesetId, + createdByUserId: thomasEmrax.userId, + referencedRule: { + connect: [{ ruleId: vehicleConfigRule.ruleId }, { ruleId: wheelRule.ruleId }] + } + } + }); + + const brakePedalRule = await prisma.rule.create({ + data: { + ruleCode: 'T.2.1.1', + ruleContent: 'The brake pedal must be capable of locking all four wheels in both dry and wet conditions', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: brakingRule.ruleId, + createdByUserId: joeShmoe.userId + } + }); + + // Electrical System Rules with References + const electricalSystemRule = await prisma.rule.create({ + data: { + ruleCode: 'T.3.1', + ruleContent: 'Electrical System - All high voltage components must be protected and isolated per safety requirements', + rulesetId: fsae2025Ruleset.rulesetId, + createdByUserId: thomasEmrax.userId, + imageFileIds: ['electrical-diagram-1', 'electrical-diagram-2', 'electrical-diagram-3'], + referencedRule: { + connect: [{ ruleId: powertrainRule.ruleId }, { ruleId: safetyRule.ruleId }] + } + } + }); + + const shutdownCircuitRule = await prisma.rule.create({ + data: { + ruleCode: 'T.3.1.1', + ruleContent: 'A shutdown circuit must be installed that disables the tractive system when activated', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: electricalSystemRule.ruleId, + createdByUserId: joeBlow.userId + } + }); + + const shutdownButtonRule = await prisma.rule.create({ + data: { + ruleCode: 'T.3.1.2', + ruleContent: 'Shutdown buttons must be located on both sides of the vehicle and be easily accessible', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: electricalSystemRule.ruleId, + createdByUserId: thomasEmrax.userId + } + }); + + // Accumulator Container Rules + const accumulatorRule = await prisma.rule.create({ + data: { + ruleCode: 'T.3.2', + ruleContent: 'Accumulator Container - The accumulator container must protect the cells from impact and debris', + rulesetId: fsae2025Ruleset.rulesetId, + createdByUserId: batman.userId, + referencedRule: { + connect: [{ ruleId: safetyRule.ruleId }] + } + } + }); + + const accumulatorMountingRule = await prisma.rule.create({ + data: { + ruleCode: 'T.3.2.1', + ruleContent: 'The accumulator container must be rigidly mounted to the frame', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: accumulatorRule.ruleId, + createdByUserId: thomasEmrax.userId + } + }); + + // General Rules (Orphan - no parent) + const generalRule = await prisma.rule.create({ + data: { + ruleCode: 'G.1', + ruleContent: + 'General - All rules are subject to interpretation by competition officials. When in doubt, contact the rules committee', + rulesetId: fsae2025Ruleset.rulesetId, + createdByUserId: batman.userId + } + }); + + const competitionEligibilityRule = await prisma.rule.create({ + data: { + ruleCode: 'G.2', + ruleContent: 'Competition Eligibility - Teams must register before the deadline and submit all required documentation', + rulesetId: fsae2025Ruleset.rulesetId, + createdByUserId: superman.userId + } + }); + + // Driver Requirements + const driverRule = await prisma.rule.create({ + data: { + ruleCode: 'S.2', + ruleContent: 'Driver Requirements - All drivers must meet safety equipment and training requirements', + rulesetId: fsae2025Ruleset.rulesetId, + createdByUserId: superman.userId + } + }); + + const helmetRule = await prisma.rule.create({ + data: { + ruleCode: 'S.2.1', + ruleContent: 'Helmet - Driver must wear a helmet meeting Snell SA2020, FIA 8859-2015, or equivalent standards', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: driverRule.ruleId, + createdByUserId: batman.userId + } + }); + + const suitRule = await prisma.rule.create({ + data: { + ruleCode: 'S.2.2', + ruleContent: 'Suit - Driver must wear a driving suit meeting SFI 3.2A/1 or FIA 8856-2000 standards', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: driverRule.ruleId, + createdByUserId: superman.userId + } + }); + + // Suspension Rules + const suspensionRule = await prisma.rule.create({ + data: { + ruleCode: 'T.4.1', + ruleContent: 'Suspension - All vehicles must have a fully operational suspension system on all wheels', + rulesetId: fsae2025Ruleset.rulesetId, + createdByUserId: thomasEmrax.userId, + referencedRule: { + connect: [{ ruleId: wheelRule.ruleId }] + } + } + }); + + const suspensionTravelRule = await prisma.rule.create({ + data: { + ruleCode: 'T.4.1.1', + ruleContent: 'The suspension must have at least 50.8 mm (2 inches) of travel', + rulesetId: fsae2025Ruleset.rulesetId, + parentRuleId: suspensionRule.ruleId, + createdByUserId: joeShmoe.userId + } + }); + + // Adding some rules to the 2024 ruleset as well + const tech2024Rule = await prisma.rule.create({ + data: { + ruleCode: 'T.1', + ruleContent: 'Technical Rules - 2024 Edition', + rulesetId: fsae2024Ruleset.rulesetId, + createdByUserId: batman.userId + } + }); + + const vehicle2024Rule = await prisma.rule.create({ + data: { + ruleCode: 'T.1.1', + ruleContent: 'Vehicle must be four-wheeled (2024 rules)', + rulesetId: fsae2024Ruleset.rulesetId, + parentRuleId: tech2024Rule.ruleId, + createdByUserId: thomasEmrax.userId + } + }); }; performSeed() diff --git a/src/backend/src/routes/rules.routes.ts b/src/backend/src/routes/rules.routes.ts new file mode 100644 index 0000000000..310552febe --- /dev/null +++ b/src/backend/src/routes/rules.routes.ts @@ -0,0 +1,108 @@ +import express from 'express'; +import RulesController from '../controllers/rules.controllers'; +import { nonEmptyString, validateInputs } from '../utils/validation.utils'; +import { body } from 'express-validator'; +import { MAX_FILE_SIZE } from 'shared'; +import multer, { memoryStorage } from 'multer'; + +const rulesRouter = express.Router(); + +rulesRouter.get('/rulesetType/:rulesetTypeId/active', RulesController.getActiveRuleset); +rulesRouter.get('/ruleset/:rulesetId', RulesController.getRulesetById); + +rulesRouter.post( + '/rule/create', + nonEmptyString(body('ruleCode')), + nonEmptyString(body('ruleContent')), + nonEmptyString(body('rulesetId')), + body('parentRuleId').optional().isString(), + body('referencedRules').optional().isArray(), + body('referencedRules.*').optional().isString(), + body('imageFileIds').optional().isArray(), + body('imageFileIds.*').optional().isString(), + validateInputs, + RulesController.createRule +); +rulesRouter.post( + '/rule/:ruleId/edit', + nonEmptyString(body('ruleContent')), + body('ruleCode').optional().isString(), + body('imageFileIds').optional().isArray(), + body('imageFileIds.*').optional().isString(), + body('parentRuleId').optional().isString(), + validateInputs, + RulesController.editRule +); +rulesRouter.post('/rule/:ruleId/delete', RulesController.deleteRule); + +rulesRouter.post('/rulesetType/create', nonEmptyString(body('name')), validateInputs, RulesController.createRulesetType); + +rulesRouter.post( + '/projectRule/create', + nonEmptyString(body('ruleId')), + nonEmptyString(body('projectId')), + validateInputs, + RulesController.createProjectRule +); + +rulesRouter.get('/rulesetTypes', RulesController.getAllRulesetTypes); +rulesRouter.get('/ruleset/:rulesetId/rules/unassigned', RulesController.getUnassignedRules); +rulesRouter.post('/ruleset/:rulesetId/delete', RulesController.deleteRuleset); +rulesRouter.post('/projectRule/:projectRuleId/delete', RulesController.deleteProjectRule); + +rulesRouter.get('/rulesets/:rulesetTypeId', RulesController.getRulesetsByRulesetType); +rulesRouter.post( + '/projectRule/:projectRuleId/editStatus', + nonEmptyString(body('newStatus')), + validateInputs, + RulesController.editProjectRuleStatus +); + +rulesRouter.post( + '/rule/:ruleId/toggle-team', + nonEmptyString(body('teamId')), + validateInputs, + RulesController.toggleRuleTeam +); + +rulesRouter.post( + '/ruleset/create', + nonEmptyString(body('name')), + nonEmptyString(body('rulesetTypeId')), + body('carNumber').isInt(), + body('active').isBoolean(), + nonEmptyString(body('fileId')), + validateInputs, + RulesController.createRuleset +); +rulesRouter.post('/rulesetType/:rulesetTypeId/delete', RulesController.deleteRulesetType); +rulesRouter.get('/:rulesetTypeId/team/:teamId', RulesController.getTeamRulesInRulesetType); + +rulesRouter.post( + '/ruleset/:rulesetId/update', + body('isActive').isBoolean(), + nonEmptyString(body('name')), + validateInputs, + RulesController.updateRuleset +); +rulesRouter.get('/ruleset/:rulesetId/team/:teamId/rules/unassigned', RulesController.getUnassignedRulesForRuleset); + +rulesRouter.get('/ruleset/:rulesetId/project/:projectId/rules', RulesController.getProjectRules); + +rulesRouter.get('/:ruleId/subrules', RulesController.getChildRules); +rulesRouter.get('/:rulesetId/parentRules', RulesController.getTopLevelRules); +rulesRouter.get('/ruleset/:rulesetId', RulesController.getSingleRuleset); +rulesRouter.get('/:rulesetTypeId', RulesController.getRulesetType); + +rulesRouter.post( + '/ruleset/:rulesetId/parse', + nonEmptyString(body('fileId')), + nonEmptyString(body('parserType')), // 'FSAE' or 'FHE' + validateInputs, + RulesController.parseRuleset +); + +const upload = multer({ limits: { fileSize: MAX_FILE_SIZE }, storage: memoryStorage() }); +rulesRouter.post('/upload/file', upload.single('file'), RulesController.uploadRulesetFile); + +export default rulesRouter; diff --git a/src/backend/src/services/rules.services.ts b/src/backend/src/services/rules.services.ts new file mode 100644 index 0000000000..b1cc0c5e0b --- /dev/null +++ b/src/backend/src/services/rules.services.ts @@ -0,0 +1,1466 @@ +import { Organization, Rule, Rule_Completion } from '@prisma/client'; +import { + isAdmin, + isLeadership, + ProjectRule, + RulesetType, + notGuest, + User, + Rule as SharedRule, + isHead, + Ruleset +} from 'shared'; +import prisma from '../prisma/prisma'; +import { + AccessDeniedAdminOnlyException, + AccessDeniedGuestException, + AccessDeniedException, + DeletedException, + HttpException, + InvalidOrganizationException, + NotFoundException +} from '../utils/errors.utils'; +import { userHasPermission } from '../utils/users.utils'; +import { + getProjectRuleQueryArgs, + getRulesetQueryArgs, + getRulePreviewQueryArgs +} from '../prisma-query-args/rules.query-args'; +import { + ruleTransformer, + projectRuleTransformer, + rulesetTransformer, + rulesetTypeTransformer +} from '../transformers/rules.transformer'; +import { ParsedRule, parseRulesFromPdf } from '../utils/parse.utils'; +import { uploadFile, downloadFile } from '../utils/google-integration.utils'; + +export default class RulesService { + /** + * Gets the active ruleset for the given ruleset type ID + * @param user a user who is requesting for the active ruleset + * @param rulesetTypeId the given ruleset type id + * @param organization the organization for permission check + * @returns a ruleset with the given id if it exists, otherwise throws an error + */ + static async getActiveRuleset(user: User, rulesetTypeId: string, organization: Organization) { + if (!(await userHasPermission(user.userId, organization.organizationId, notGuest))) + throw new AccessDeniedException('only members and above can view ruleset types!'); + + const rulesetType = await prisma.ruleset_Type.findUnique({ + where: { rulesetTypeId, organizationId: organization.organizationId } + }); + + if (!rulesetType) { + throw new NotFoundException('Ruleset Type', rulesetTypeId); + } + + if (rulesetType?.deletedByUserId != null) { + throw new DeletedException('Ruleset Type', rulesetTypeId); + } + + const activeRuleset = await prisma.ruleset.findFirst({ + where: { rulesetTypeId, deletedByUserId: null, active: true }, + ...getRulesetQueryArgs() + }); + + if (!activeRuleset) { + throw new NotFoundException('Active Ruleset for given Ruleset Type', rulesetTypeId); + } + + return rulesetTransformer(activeRuleset); + } + + /** + * Gets a single ruleset by its ID + * @param rulesetId The ID of the ruleset to retrieve + * @param organizationId The ID of the organization the ruleset belongs to + * @returns The ruleset if found, otherwise throws an error + */ + static async getRulesetById(rulesetId: string, organizationId: string): Promise { + const ruleset = await prisma.ruleset.findFirst({ + where: { + rulesetId, + deletedByUserId: null, + rulesetType: { + organizationId + } + }, + ...getRulesetQueryArgs() + }); + + if (!ruleset) { + throw new NotFoundException('Ruleset', rulesetId); + } + + return rulesetTransformer(ruleset); + } + + /** + * Creates a new rule in the database + * + * @param user The user creating the rule, must be a member or above + * @param ruleCode The unique code identifier for the rule (e.g., "T.1.1.1") + * @param ruleContent The text content of the rule + * @param rulesetId The ID of the ruleset this rule belongs to + * @param organization The organization the rule belongs to + * @param parentRuleId Optional ID of the parent rule if this is a sub-rule + * @param referencedRuleIds Optional array of rule IDs that this rule references + * @param imageFileIds Optional array of Google Drive file IDs for images + * @returns The created rule + */ + static async createRule( + user: User, + ruleCode: string, + ruleContent: string, + rulesetId: string, + organization: Organization, + parentRuleId?: string, + referencedRuleIds: string[] = [], + imageFileIds: string[] = [] + ) { + // Check user has permission (members and above) + if (!(await userHasPermission(user.userId, organization.organizationId, notGuest))) { + throw new AccessDeniedException('Only members and above can create rules'); + } + + // Verify ruleset exists and belongs to organization + const ruleset = await prisma.ruleset.findUnique({ + where: { rulesetId }, + include: { + car: { + include: { + wbsElement: true + } + } + } + }); + + if (!ruleset) { + throw new NotFoundException('Ruleset', rulesetId); + } + + if (ruleset.deletedByUserId) { + throw new DeletedException('Ruleset', rulesetId); + } + + if (ruleset.car.wbsElement.organizationId !== organization.organizationId) { + throw new AccessDeniedException('Cannot create rule in a ruleset from another organization'); + } + + // Check for duplicate rule code within the same ruleset + const existingRule = await prisma.rule.findUnique({ + where: { + rulesetId_ruleCode: { + rulesetId, + ruleCode + } + } + }); + + if (existingRule) { + throw new HttpException(400, `Rule with code ${ruleCode} already exists in this ruleset`); + } + + // Verify parent rule exists if provided + if (parentRuleId) { + const parentRule = await prisma.rule.findUnique({ + where: { ruleId: parentRuleId } + }); + + if (!parentRule) { + throw new NotFoundException('Parent Rule', parentRuleId); + } + + if (parentRule.dateDeleted) { + throw new DeletedException('Parent Rule', parentRuleId); + } + + if (parentRule.rulesetId !== rulesetId) { + throw new HttpException(400, 'Parent rule must be in the same ruleset'); + } + } + + // Verify referenced rules exist + if (referencedRuleIds.length > 0) { + const referencedRules = await prisma.rule.findMany({ + where: { + ruleId: { in: referencedRuleIds } + } + }); + + if (referencedRules.length !== referencedRuleIds.length) { + throw new NotFoundException('Referenced Rule', 'provided IDs'); + } + + const deletedReferencedRule = referencedRules.find((rule) => rule.dateDeleted !== null); + if (deletedReferencedRule) { + throw new DeletedException('Referenced Rule', deletedReferencedRule.ruleId); + } + } + + // Create the rule + const rule = await prisma.rule.create({ + data: { + ruleCode, + ruleContent, + imageFileIds, + ruleset: { connect: { rulesetId } }, + createdBy: { connect: { userId: user.userId } }, + ...(parentRuleId && { parentRule: { connect: { ruleId: parentRuleId } } }), + ...(referencedRuleIds.length > 0 && { + referencedRule: { + connect: referencedRuleIds.map((id) => ({ ruleId: id })) + } + }) + }, + ...getRulePreviewQueryArgs() + }); + + return ruleTransformer(rule); + } + + /** + * Creates new ruleset type with the given information + * @param submitter a user who is making this request + * @param name the name of the ruleset type + * @param organizationId the organization ID for permission check + * @returns A newly created ruleset type + */ + static async createRulesetType(submitter: User, name: string, organization: Organization) { + if (!(await userHasPermission(submitter.userId, organization.organizationId, isLeadership))) + throw new AccessDeniedException('only leadership and above can create ruleset types!'); + + const rulesetType = await prisma.ruleset_Type.create({ + data: { + name, + createdByUserId: submitter.userId, + organizationId: organization.organizationId + } + }); + + return rulesetType; + } + + /** + * Deletes a rule + * @param ruleId id of a rule to be deleted + * @param deleter user deleting the rule + * @param org the org of the user deleting the rule + * @returns the deleted rule + */ + static async deleteRule(ruleId: string, deleter: User, org: Organization): Promise { + const rule = await prisma.rule.findUnique({ + where: { ruleId }, + include: { + ruleset: { + include: { + car: { + include: { + wbsElement: true + } + } + } + } + } + }); + + if (!(await userHasPermission(deleter.userId, org.organizationId, isAdmin))) { + throw new AccessDeniedAdminOnlyException('delete rules'); + } + + if (!rule) throw new NotFoundException('Rule', ruleId); + if (rule.dateDeleted) throw new DeletedException('Rule', ruleId); + + if (rule.ruleset?.car?.wbsElement?.organizationId !== org.organizationId) throw new InvalidOrganizationException('Rule'); + + await prisma.$transaction(async (tx) => { + const deleteParentChildReferencing = async (currRuleId: string): Promise => { + const referencingRules = await tx.rule.findMany({ + where: { + referencedRule: { + some: { ruleId: currRuleId } + }, + dateDeleted: null + }, + select: { ruleId: true } + }); + + for (const referencingRule of referencingRules) { + await tx.rule.update({ + where: { ruleId: referencingRule.ruleId }, + data: { + referencedRule: { + disconnect: { ruleId: currRuleId } + } + } + }); + } + + const childRules = await tx.rule.findMany({ + where: { + parentRuleId: currRuleId, + dateDeleted: null + } + }); + for (const childRule of childRules) { + await deleteParentChildReferencing(childRule.ruleId); + } + + await tx.rule.update({ + where: { ruleId: currRuleId }, + data: { + dateDeleted: new Date(), + deletedByUserId: deleter.userId + } + }); + }; + + await deleteParentChildReferencing(ruleId); + }); + + const deletedRule = await prisma.rule.findUnique({ + where: { ruleId } + }); + + return deletedRule!; + } + + /** + * Add a preexisting rule to a specific project + * + * @param submitter The user creating the project rule + * @param organization The organization the project rule is being created in + * @param ruleId The rule ID being added to the project + * @param projectId The project ID to add the rule to + * @returns The created project rule + */ + static async createProjectRule( + submitter: User, + organization: Organization, + ruleId: string, + projectId: string + ): Promise { + if (!(await userHasPermission(submitter.userId, organization.organizationId, isLeadership))) { + throw new AccessDeniedException('You do not have permissions to assign rules to projects'); + } + + const rule = await prisma.rule.findUnique({ + where: { ruleId }, + include: { + subRules: true, + ruleset: { select: { car: { include: { wbsElement: { select: { organizationId: true } } } } } } + } + }); + + if (!rule) { + throw new NotFoundException('Rule', ruleId); + } + if (rule.ruleset.car.wbsElement.organizationId !== organization.organizationId) { + throw new InvalidOrganizationException('Rule'); + } + + if (rule.dateDeleted) throw new DeletedException('Rule', ruleId); + + const project = await prisma.project.findUnique({ + where: { projectId }, + include: { wbsElement: true } + }); + + if (!project) { + throw new NotFoundException('Project', projectId); + } + if (project.wbsElement.organizationId !== organization.organizationId) { + throw new InvalidOrganizationException('Project'); + } + + if (project.wbsElement.dateDeleted) throw new DeletedException('Project', projectId); + + // Checks if this rule was already assigned to this project + const existingProjectRule = await prisma.project_Rule.findUnique({ + where: { ruleId_projectId: { ruleId, projectId } } + }); + + if (existingProjectRule) { + throw new HttpException(400, 'This rule is already associated with the project'); + } + + const projectRule = await prisma.project_Rule.create({ + data: { + ruleId, + projectId, + currentStatus: Rule_Completion.REVIEW, + createdByUserId: submitter.userId + }, + ...getProjectRuleQueryArgs() + }); + + return projectRuleTransformer(projectRule); + } + + /** + * Edits a rule with the given id + * @param submitter a user who is making this request + * @param ruleContent the rule content to edit + * @param ruleId The rule ID being edited + * @param ruleCode The rule code to update (optional, keeps existing if not provided) + * @param imageFileIds The image files to update (optional, keeps existing if not provided) + * @param parentRuleId The parent rule ID to update + * @param organization the organization the rule belongs to + * @returns the edited rule + */ + static async editRule( + submitter: User, + ruleContent: string, + ruleId: string, + ruleCode: string | undefined, + imageFileIds: string[] | undefined, + organization: Organization, + parentRuleId?: string + ) { + if (!(await userHasPermission(submitter.userId, organization.organizationId, isAdmin))) + throw new AccessDeniedAdminOnlyException('edit a rule'); + + const currentRule = await prisma.rule.findUnique({ + where: { ruleId }, + include: { + ruleset: { + include: { + car: { + include: { + wbsElement: true + } + } + } + } + } + }); + + if (!currentRule) { + throw new NotFoundException('Rule', ruleId); + } + + if (currentRule.dateDeleted) { + throw new DeletedException('Rule', ruleId); + } + + if (currentRule.ruleset?.car?.wbsElement?.organizationId !== organization.organizationId) + throw new InvalidOrganizationException('Rule'); + + if (parentRuleId) { + const parentRule = await prisma.rule.findUnique({ + where: { ruleId: parentRuleId } + }); + + if (!parentRule) { + throw new NotFoundException('Parent Rule', parentRuleId); + } + + if (parentRule.dateDeleted) { + throw new DeletedException('Parent Rule', parentRuleId); + } + } + + const updatedRule = await prisma.rule.update({ + where: { + ruleId + }, + data: { + ruleContent, + ...(ruleCode !== undefined && { ruleCode }), + ...(imageFileIds !== undefined && { imageFileIds }), + ...(parentRuleId && { parentRuleId }), + dateUpdated: new Date(), + updatedByUserId: submitter.userId + }, + ...getRulePreviewQueryArgs() + }); + + return ruleTransformer(updatedRule); + } + + /** + * Given a ruleset id, retrieves the ruleset and throws errors if + * it does not exist or is already deleted + * @param rulesetId the id of the ruleset + * @param organizationId the id of the organization the ruleset is being deleted in + * @returns the ruleset with query args + */ + static async getRulesetWithQueryArgs(rulesetId: string) { + const ruleset = await prisma.ruleset.findUnique({ + where: { rulesetId }, + ...getRulesetQueryArgs() + }); + + if (!ruleset) throw new NotFoundException('Ruleset', rulesetId); + if (ruleset.deletedByUserId) throw new DeletedException('Ruleset', rulesetId); + + return ruleset; + } + + /** + * Deletes a specific Ruleset + * @param rulesetId the id of the ruleset to be deleted + * @param deleterId the id of the user deleting the ruleset + * @param organizationID the id of the organization the ruleset is being deleted in + * @returns the deleted Ruleset + */ + static async deleteRuleset(rulesetId: string, deleterId: string, organizationId: string) { + const ruleset = await RulesService.getRulesetWithQueryArgs(rulesetId); + + const hasPermission = + (await userHasPermission(deleterId, organizationId, isAdmin)) || deleterId === ruleset.createdByUserId; + + if (!hasPermission) throw new AccessDeniedException('Only admins can delete a ruleset.'); + + if (ruleset.active) { + throw new HttpException(400, 'Cannot delete an active ruleset. Please deactivate it first.'); + } + + const deletedRuleset = await prisma.ruleset.update({ + where: { rulesetId }, + data: { deletedBy: { connect: { userId: deleterId } }, active: false }, + ...getRulesetQueryArgs() + }); + + return rulesetTransformer(deletedRuleset); + } + + static async getAllRulesetTypes(organization: Organization): Promise { + const rulesets = await prisma.ruleset_Type.findMany({ + where: { + organizationId: organization.organizationId, + deletedByUserId: null + }, + include: { + revisionFiles: true + } + }); + return rulesets.map(rulesetTypeTransformer); + } + + /** + * Gets a ruleset type for a given ruleset type ID + * @param rulesetTypeId id of ruleset type + * @param organizationId id of organization + * @returns ruleset type associated with provided ruleset type ID + */ + static async getRulesetType(rulesetTypeId: string, organizationId: string): Promise { + const rulesetType = await prisma.ruleset_Type.findUnique({ + where: { + rulesetTypeId, + organizationId, + deletedBy: null + }, + include: { + revisionFiles: true + } + }); + + if (!organizationId) { + throw new NotFoundException('Organization', organizationId); + } + + if (!rulesetType) { + throw new NotFoundException('Ruleset Type', rulesetTypeId); + } + + return rulesetTypeTransformer(rulesetType); + } + + /** + * Gets rulesets for a given ruleset type + * @param rulesetTypeId id of ruleset type + * @param organizationId id of organization + * @returns rulesets associated with provided ruleset type + */ + static async getRulesetsByRulesetType(rulesetTypeId: string, organizationId: string): Promise { + const rulesets = await prisma.ruleset.findMany({ + where: { + rulesetTypeId, + deletedByUserId: null, + rulesetType: { + organizationId + } + }, + orderBy: { + dateCreated: 'desc' + }, + ...getRulesetQueryArgs() + }); + + return rulesets.map(rulesetTransformer); + } + + /** + * Gets all rules assigned to a team that are in the active ruleset of a given ruleset type + * @param teamId id of the team + * @param rulesetTypeId id of ruleset type + * @param organization the organization + * @returns array of rule previews + */ + static async getTeamRulesInRulesetType(teamId: string, rulesetTypeId: string, organization: Organization) { + const team = await prisma.team.findUnique({ + where: { teamId, dateArchived: null } + }); + + if (!team) { + throw new NotFoundException('Team', teamId); + } + + if (team.organizationId !== organization.organizationId) { + throw new InvalidOrganizationException('Team'); + } + + const rulesetType = await prisma.ruleset_Type.findUnique({ + where: { rulesetTypeId } + }); + + if (!rulesetType) { + throw new NotFoundException('Ruleset Type', rulesetTypeId); + } + + if (rulesetType.deletedByUserId) { + throw new DeletedException('Ruleset Type', rulesetTypeId); + } + + if (rulesetType.organizationId !== organization.organizationId) { + throw new InvalidOrganizationException('Ruleset Type'); + } + + const activeRuleset = await prisma.ruleset.findFirst({ + where: { + rulesetTypeId, + active: true, + deletedByUserId: null + }, + ...getRulesetQueryArgs() + }); + + if (!activeRuleset) { + throw new NotFoundException('Active Ruleset for given Ruleset Type', rulesetTypeId); + } + + const rules = await prisma.rule.findMany({ + where: { + rulesetId: activeRuleset.rulesetId, + dateDeleted: null, + teams: { + some: { + teamId + } + } + }, + ...getRulePreviewQueryArgs() + }); + + return rules.map(ruleTransformer); + } + + /** + * Updates the status of a project rule + * Such as changing a project rule from INCOMPLETE to COMPLETED + * @param submitter the user updating the status + * @param organization the organization of the rule + * @param projectRuleId the id of the project rule to update + * @param newStatus the new status of the project rule + * @returns the project rule with updated status + */ + static async editProjectRuleStatus( + submitter: User, + organization: Organization, + projectRuleId: string, + newStatus: Rule_Completion + ): Promise { + // Ensure new satus is a valid Rule_Completion value + if (!Object.values(Rule_Completion).includes(newStatus as Rule_Completion)) { + throw new HttpException(400, `status must be one of: ${Object.values(Rule_Completion).join(', ')}`); + } + + if (!(await userHasPermission(submitter.userId, organization.organizationId, isLeadership))) { + throw new AccessDeniedException('You do not have permissions to update a project rule status'); + } + + const projectRule = await prisma.project_Rule.findUnique({ + where: { projectRuleId }, + include: { rule: { include: { ruleset: { include: { car: { include: { wbsElement: true } } } } } } } + }); + + if (!projectRule) { + throw new NotFoundException('Project Rule', projectRuleId); + } + + if (projectRule.rule.ruleset.car.wbsElement.organizationId !== organization.organizationId) { + throw new InvalidOrganizationException('Project Rule'); + } + + // If the status does not change, simply return the project rule + if (projectRule.currentStatus === newStatus) { + const originalProjectRule = await prisma.project_Rule.findUnique({ + where: { projectRuleId }, + ...getProjectRuleQueryArgs() + }); + return projectRuleTransformer(originalProjectRule); + } + + const newStatusHistory = { + createdByUserId: submitter.userId, + newStatus, + note: `${submitter.firstName} ${submitter.lastName} marked as ${newStatus}` + }; + + const updatedProjectRule = await prisma.project_Rule.update({ + where: { projectRuleId }, + data: { currentStatus: newStatus, statusHistory: { create: newStatusHistory } }, + ...getProjectRuleQueryArgs() + }); + + return projectRuleTransformer(updatedProjectRule); + } + + /** + * Assigns a rule to a team. If the team already is assigned to the + * rule, removes the team from the rule. + * @param ruleId The ruleId of the rule to be added to + * @param teamIds The team to be added to the rule + * @param user The user adding the team to the rule + * @param org The organization the rule belongs to + * @returns the updated rule + * @throws If the user is a guest, the rule does not exist or + * is deleted, or a team does not exist, is in the wrong + * organization, or is archived. + * + */ + static async toggleRuleTeam(ruleId: string, teamId: string, user: User, org: Organization) { + // Checks that the user is not a guest + if (!(await userHasPermission(user.userId, org.organizationId, notGuest))) { + throw new AccessDeniedGuestException('Toggle Rule Team'); + } + + // Checks that the rule exists and is not deleted + const rule = await prisma.rule.findUnique({ + where: { ruleId }, + include: { + teams: true, + ruleset: { select: { car: { include: { wbsElement: { select: { organizationId: true } } } } } } + } + }); + if (!rule) { + throw new NotFoundException('Rule', ruleId); + } + if (rule.deletedByUserId) { + throw new DeletedException('Rule', ruleId); + } + if (rule.ruleset.car.wbsElement.organizationId !== org.organizationId) { + throw new InvalidOrganizationException('Rule'); + } + + // Checks based on the team + const team = await prisma.team.findUnique({ where: { teamId } }); + if (!team) throw new NotFoundException('Team', teamId); + if (team.organizationId !== org.organizationId) throw new InvalidOrganizationException('Rule'); + if (team.dateArchived) throw new HttpException(400, 'Cannot toggle an archived team.'); + + // We add the team to the rule if it is not already in the rule + // If the rule is not in this team, add the team to the rule + // If the rule is already in this team, remove the team from the rule + if (!rule.teams.some((currTeam) => currTeam.teamId === teamId)) { + await prisma.rule.update({ + where: { ruleId: rule.ruleId }, + data: { + teams: { + connect: { + teamId + } + } + } + }); + } else { + await prisma.rule.update({ + where: { ruleId: rule.ruleId }, + data: { + teams: { + disconnect: { + teamId + } + } + } + }); + } + + // retrieve and return the updated rule + const newRule = await prisma.rule.findUnique({ + where: { ruleId }, + ...getRulePreviewQueryArgs() + }); + + return ruleTransformer(newRule!); + } + + static async createRuleset( + submitter: User, + organization: Organization, + name: string, + rulesetTypeId: string, + carNumber: number, + active: boolean, + fileId: string + ) { + if (!(await userHasPermission(submitter.userId, organization.organizationId, isLeadership))) { + throw new AccessDeniedException('only leadership and above can create ruleset!'); + } + + const rulesetType = await prisma.ruleset_Type.findUnique({ + where: { + rulesetTypeId + } + }); + + if (!rulesetType) { + throw new NotFoundException('Ruleset Type', rulesetTypeId); + } + if (rulesetType.dateDeleted !== null) { + throw new DeletedException('Ruleset Type', rulesetTypeId); + } + + if (rulesetType.organizationId !== organization.organizationId) throw new InvalidOrganizationException('Ruleset Type'); + + const car = await prisma.car.findFirst({ + where: { + wbsElement: { + carNumber, + organizationId: organization.organizationId, + dateDeleted: null + } + }, + include: { wbsElement: true } + }); + + if (!car) { + throw new NotFoundException('Car', carNumber); + } + + const ruleset = await prisma.ruleset.create({ + data: { + fileId, + rulesetTypeId, + name, + carId: car.carId, + active, + createdByUserId: submitter.userId + }, + ...getRulesetQueryArgs() + }); + + return rulesetTransformer(ruleset); + } + + /** + * Deletes a ruleset type and all the rulesets in the ruleset type's revision files. + * + * @param user The user who is deleting the ruleset type + * @param rulesetTypeId The ruleset type to be deleted + * @param organization The organization that the ruleset is being deleted for + */ + static async deleteRulesetType(deleter: User, id: string, organization: Organization): Promise { + // check if user is admin + if (!(await userHasPermission(deleter.userId, organization.organizationId, isAdmin))) { + throw new AccessDeniedAdminOnlyException('delete ruleset types'); + } + + const rulesetType = await prisma.ruleset_Type.findUnique({ + where: { rulesetTypeId: id, organizationId: organization.organizationId }, + include: { + revisionFiles: true + } + }); + + if (!rulesetType) { + throw new NotFoundException('Ruleset Type', id); + } + if (rulesetType.deletedByUserId) { + throw new DeletedException('Ruleset Type', id); + } + + await prisma.$transaction(async (tx) => { + // delete all rulesets in revision files + for (const ruleset of rulesetType.revisionFiles) { + await tx.ruleset.update({ + where: { rulesetId: ruleset.rulesetId }, + data: { deletedByUserId: deleter.userId } + }); + } + + // delete the actual ruleset type itself + await tx.ruleset_Type.update({ + where: { rulesetTypeId: id }, + data: { deletedByUserId: deleter.userId } + }); + }); + + const deletedRule = await prisma.ruleset_Type.findUnique({ + where: { rulesetTypeId: id }, + include: { + revisionFiles: true + } + }); + + return rulesetTypeTransformer(deletedRule); + } + + /** + * Deletes a project rule and its associated rule status changes + * @param projectRuleId The ID of the project rule to delete + * @param deleter The user deleting the project rule (must be admin) + * @param organization The organization the project rule belongs to + * @returns The deleted project rule + */ + static async deleteProjectRule(projectRuleId: string, deleter: User, organization: Organization): Promise { + if (!(await userHasPermission(deleter.userId, organization.organizationId, isHead))) { + throw new AccessDeniedAdminOnlyException('delete project rules'); + } + + const projectRule = await prisma.project_Rule.findUnique({ + where: { projectRuleId }, + include: { + project: { + include: { + wbsElement: true + } + }, + rule: { + include: { + ruleset: { + include: { + car: { + include: { + wbsElement: true + } + } + } + } + } + } + } + }); + + if (!projectRule) { + throw new NotFoundException('Project Rule', projectRuleId); + } + + if (projectRule.project.wbsElement.organizationId !== organization.organizationId) { + throw new InvalidOrganizationException('Project Rule'); + } + + if (projectRule.rule.ruleset.car.wbsElement.organizationId !== organization.organizationId) { + throw new InvalidOrganizationException('Project Rule'); + } + + if (projectRule.dateDeleted) { + throw new DeletedException('Project Rule', projectRuleId); + } + + const deletedProjectRule = await prisma.project_Rule.update({ + where: { projectRuleId }, + data: { + dateDeleted: new Date(), + deletedByUserId: deleter.userId + }, + ...getProjectRuleQueryArgs() + }); + + return projectRuleTransformer(deletedProjectRule); + } + + /** + * Updates a rulesets status + * @param submitter user updating the ruleset + * @param organizationId organization of ruleset being updated + * @param rulesetId id of ruleset being updated + * @param status new status of ruleset + * @returns + */ + static async updateRuleset(submitter: User, organizationId: string, rulesetId: string, name: string, isActive: boolean) { + if (!(await userHasPermission(submitter.userId, organizationId, isHead))) { + throw new AccessDeniedException('You do not have permissions to update ruleset status'); + } + + const rulesetExists = await prisma.ruleset.findUnique({ + where: { + rulesetId, + rulesetType: { + organizationId + }, + deletedByUserId: null + } + }); + + if (!rulesetExists) { + throw new NotFoundException('Ruleset', rulesetId); + } + + if (!rulesetExists.active && isActive) { + const activeRuleset = await prisma.ruleset.findFirst({ + where: { + active: true, + rulesetType: { + rulesetTypeId: rulesetExists.rulesetTypeId, + organizationId + }, + deletedByUserId: null + } + }); + + if (activeRuleset) { + throw new HttpException(400, 'There is already an active ruleset for this ruleset type'); + } + } + const ruleset = await prisma.ruleset.update({ + where: { + rulesetId, + rulesetType: { + organizationId + } + }, + data: { + name, + active: isActive + }, + ...getRulesetQueryArgs() + }); + + return rulesetTransformer(ruleset); + } + + /** + * Gets all subrules of a specific rule. + * @param ruleId the ID of the parent rule + * @param organization the organization the rule belongs to + * @returns an array of all child rules (the Rule object) + */ + static async getChildRules(ruleId: string, organization: Organization): Promise { + // Verify the parent rule exists and belongs to the organization + const parentRule = await prisma.rule.findUnique({ + where: { ruleId }, + include: { + ruleset: { + include: { + car: { + include: { + wbsElement: true + } + } + } + } + } + }); + + if (!parentRule) { + throw new NotFoundException('Rule', ruleId); + } + + if (parentRule.dateDeleted) { + throw new DeletedException('Rule', ruleId); + } + + if (parentRule.ruleset.car.wbsElement.organizationId !== organization.organizationId) { + throw new InvalidOrganizationException('Rule'); + } + + const subRules = await prisma.rule.findMany({ + where: { + parentRuleId: ruleId, + dateDeleted: null + }, + ...getRulePreviewQueryArgs() + }); + return subRules.map((rule) => ruleTransformer(rule)); + } + + /** + * Gets all unassigned rules (rules with no team assignments) for a given ruleset + * @param rulesetId the id of the ruleset + * @param organization the organization the ruleset belongs to + * @returns an array of rules with no team assignments, ordered by ruleCode ascending + */ + static async getUnassignedRules(rulesetId: string, organization: Organization): Promise { + const ruleset = await prisma.ruleset.findUnique({ + where: { rulesetId }, + include: { + car: { + include: { + wbsElement: true + } + } + } + }); + + if (!ruleset) { + throw new NotFoundException('Ruleset', rulesetId); + } + + if (ruleset.deletedByUserId) { + throw new DeletedException('Ruleset', rulesetId); + } + + if (ruleset.car.wbsElement.organizationId !== organization.organizationId) { + throw new InvalidOrganizationException('Ruleset'); + } + + const rules = await prisma.rule.findMany({ + where: { + rulesetId, + dateDeleted: null, + teams: { + none: {} + } + }, + orderBy: { + ruleCode: 'asc' + }, + ...getRulePreviewQueryArgs() + }); + + return rules.map(ruleTransformer); + } + + /** + * Gets team rules that are unassigned to a project + * @param rulesetId ruleset the rules are in + * @param teamId team that rules are assigned to + * @param organizationId the organization id + * @returns the rules in this team that do not have an associated project rule + */ + static async getUnassignedRulesForRuleset(rulesetId: string, teamId: string, organizationId: string) { + const ruleset = await prisma.ruleset.findUnique({ + where: { rulesetId }, + select: { + dateDeleted: true, + rulesetType: { + select: { + organizationId: true + } + } + } + }); + + if (!ruleset) { + throw new NotFoundException('Ruleset', rulesetId); + } + + if (ruleset.dateDeleted) { + throw new DeletedException('Ruleset', rulesetId); + } + + if (ruleset.rulesetType.organizationId !== organizationId) { + throw new InvalidOrganizationException('Ruleset'); + } + + const team = await prisma.team.findUnique({ + where: { teamId }, + select: { + organizationId: true + } + }); + + if (!team) { + throw new NotFoundException('Team', teamId); + } + + if (team.organizationId !== organizationId) { + throw new InvalidOrganizationException('Team'); + } + + const rules = await prisma.rule.findMany({ + where: { + rulesetId, + teams: { + some: { + teamId, + organizationId + } + }, + projects: { + none: {} + }, + deletedByUserId: null + }, + ...getRulePreviewQueryArgs(), + orderBy: { + ruleCode: 'asc' + } + }); + return rules.map(ruleTransformer); + } + + /** + * Gets all rules associated with a specific project and ruleset + * @param rulesetId the id of the ruleset + * @param projectId the id of the project + * @param organization the organization the project and ruleset belong to + * @returns Array of ProjectRule objects + */ + static async getProjectRules(rulesetId: string, projectId: string, organization: Organization): Promise { + const ruleset = await prisma.ruleset.findUnique({ + where: { rulesetId }, + include: { + car: { + include: { + wbsElement: true + } + } + } + }); + + if (!ruleset) { + throw new NotFoundException('Ruleset', rulesetId); + } + + if (ruleset.deletedByUserId) { + throw new DeletedException('Ruleset', rulesetId); + } + + if (ruleset.car.wbsElement.organizationId !== organization.organizationId) { + throw new InvalidOrganizationException('Ruleset'); + } + + const project = await prisma.project.findUnique({ + where: { projectId }, + include: { + wbsElement: true + } + }); + + if (!project) { + throw new NotFoundException('Project', projectId); + } + + if (project.wbsElement.dateDeleted) { + throw new DeletedException('Project', projectId); + } + + if (project.wbsElement.organizationId !== organization.organizationId) { + throw new InvalidOrganizationException('Project'); + } + + const projectRules = await prisma.project_Rule.findMany({ + where: { + projectId, + rule: { + rulesetId, + dateDeleted: null + }, + dateDeleted: null + }, + ...getProjectRuleQueryArgs() + }); + + return projectRules.map(projectRuleTransformer); + } + + /** + * Gets all rules with no parent id + * @param rulesetId id of ruleset + * @returns an array of rules with no parent Id + */ + static async getTopLevelRules(rulesetId: string, organizationId: string) { + const ruleset = await prisma.ruleset.findUnique({ + where: { rulesetId }, + select: { + dateDeleted: true, + rulesetType: { + select: { + organizationId: true + } + } + } + }); + + if (!ruleset) { + throw new NotFoundException('Ruleset', rulesetId); + } + + if (ruleset.dateDeleted) { + throw new DeletedException('Ruleset', rulesetId); + } + + if (ruleset.rulesetType.organizationId !== organizationId) { + throw new InvalidOrganizationException('Ruleset'); + } + + const rules = await prisma.rule.findMany({ + where: { + rulesetId, + dateDeleted: null, + parentRuleId: null + }, + ...getRulePreviewQueryArgs() + }); + + return rules.map(ruleTransformer); + } + + /** + * Parses a PDF ruleset file and saves the extracted rules to the database. + * Extracts rules based on parser type (FSAE or FHE). + * Creates all rules in the database and then sets up parent-child relationships + * @param user user who uploaded the ruleset pdf + * @param organizationId organization id of the ruleset + * @param fileId google drive file id of the ruleset pdf + * @param rulesetId id of the ruleset to save the parsed rules into + * @param parserType type of parser to use (FSAE or FHE) + * @returns array of saved rules with parent relationships established + * @throws AccessDeniedException if user lacks permissions or ruleset belongs to another organization + * @throws NotFoundException if ruleset doesn't exist + * @throws DeletedException if ruleset has been deleted + * @throws HttpException(400) if file is not a PDF or contains no rules + * @throws HttpException(500) if PDF parsing fails + */ + static async parseRuleset( + user: User, + organizationId: string, + fileId: string, + rulesetId: string, + parserType: 'FSAE' | 'FHE' + ): Promise { + if (!(await userHasPermission(user.userId, organizationId, isLeadership))) { + throw new AccessDeniedException('You do not have permissions to upload and parse rulesets'); + } + + // Verify ruleset exists and belongs to organization + const ruleset = await prisma.ruleset.findUnique({ + where: { rulesetId }, + include: { + car: { + include: { + wbsElement: true + } + } + } + }); + + if (!ruleset) { + throw new NotFoundException('Ruleset', rulesetId); + } + + if (ruleset.deletedByUserId) { + throw new DeletedException('Ruleset', rulesetId); + } + if (ruleset.car.wbsElement.organizationId !== organizationId) { + throw new AccessDeniedException('Cannot parse rules into a ruleset from another organization'); + } + + // get file from Google Drive + const { buffer, type } = await downloadFile(fileId); + + // ensure the file is a PDF + if (type !== 'application/pdf') { + throw new HttpException(400, 'Ruleset File must be a PDF'); + } + let parsedRules: ParsedRule[]; + try { + parsedRules = await parseRulesFromPdf(buffer, parserType); + if (parsedRules.length === 0) { + throw new HttpException(400, 'No rules found in provided file'); + } + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + if (process.env && process.env.NODE_ENV === 'development') { + const message = error instanceof Error ? error.message : 'Unknown error'; + throw new HttpException(500, `Error parsing rules from PDF file: ${message}`); + } + throw new HttpException(500, 'Error parsing rules from PDF file'); + } + + await prisma.$transaction(async (tx) => { + await tx.rule.createMany({ + data: parsedRules.map((rule) => ({ + ruleCode: rule.ruleCode, + ruleContent: rule.ruleContent, + imageFileIds: [], + rulesetId, + createdByUserId: user.userId + })) + }); + + const createdRules = await tx.rule.findMany({ + where: { rulesetId }, + select: { + ruleId: true, + ruleCode: true + } + }); + + const ruleMap = new Map(); + createdRules.forEach((rule) => { + ruleMap.set(rule.ruleCode, rule.ruleId); + }); + + // update parent relationships + const parentUpdates = parsedRules + .filter((rule) => rule.parentRuleCode) + .map((rule) => { + const parentId = ruleMap.get(rule.parentRuleCode!); + const ruleId = ruleMap.get(rule.ruleCode); + + if (!parentId || !ruleId) return null; + + return tx.rule.update({ + where: { ruleId }, + data: { parentRuleId: parentId } + }); + }) + .filter(Boolean); + + await Promise.all(parentUpdates); + }); + + const savedRules = await prisma.rule.findMany({ + where: { rulesetId }, + ...getRulePreviewQueryArgs() + }); + + return savedRules.map(ruleTransformer); + } + + static async uploadRulesetFile(file: Express.Multer.File, uploader: User, organization: Organization) { + if (!(await userHasPermission(uploader.userId, organization.organizationId, isLeadership))) { + throw new AccessDeniedException('Only leadership and above can upload ruleset files'); + } + const data = await uploadFile(file); + return data.id; + } + + /** + * Gets a single ruleset by ID + * @param rulesetId the id of the ruleset + * @param user the user requesting the ruleset + * @param organization the organization the user belongs to + * @returns the ruleset with the given id + */ + static async getSingleRuleset(user: User, rulesetId: string, organization: Organization): Promise { + if (!(await userHasPermission(user.userId, organization.organizationId, notGuest))) + throw new AccessDeniedException('Only members and above can view rulesets!'); + + const ruleset = await prisma.ruleset.findUnique({ + where: { rulesetId }, + ...getRulesetQueryArgs() + }); + + if (!ruleset) { + throw new NotFoundException('Ruleset', rulesetId); + } + + if (ruleset.deletedByUserId) { + throw new DeletedException('Ruleset', rulesetId); + } + + if (ruleset.car.wbsElement.organizationId !== organization.organizationId) { + throw new InvalidOrganizationException('Ruleset'); + } + + return rulesetTransformer(ruleset); + } +} diff --git a/src/backend/src/transformers/rules.transformer.ts b/src/backend/src/transformers/rules.transformer.ts new file mode 100644 index 0000000000..8dea729b74 --- /dev/null +++ b/src/backend/src/transformers/rules.transformer.ts @@ -0,0 +1,66 @@ +import { Prisma } from '@prisma/client'; +import { Rule, ProjectRule, Ruleset, RulesetType } from 'shared'; +import { RulesetQueryArgs, RulePreviewQueryArgs } from '../prisma-query-args/rules.query-args'; + +export const ruleTransformer = (rule: Prisma.RuleGetPayload): Rule => { + return { + ruleId: rule.ruleId, + ruleCode: rule.ruleCode, + ruleContent: rule.ruleContent, + imageFileIds: rule.imageFileIds, + parentRule: rule.parentRule + ? { + ruleId: rule.parentRule.ruleId, + ruleCode: rule.parentRule.ruleCode + } + : undefined, + subRuleIds: rule.subRules.map((subRule) => subRule.ruleId), + referencedRuleIds: rule.referencedRule.map((ref) => ref.ruleId), + teams: rule.teams?.map((team) => ({ + teamId: team.teamId, + teamName: team.teamName + })) + }; +}; + +export const projectRuleTransformer = (projectRule: any): ProjectRule => { + return { + projectRuleId: projectRule.projectRuleId, + rule: ruleTransformer(projectRule.rule), + projectId: projectRule.projectId, + currentStatus: projectRule.currentStatus, + statusHistory: projectRule.statusHistory + }; +}; + +export const rulesetTypeTransformer = (rulesetType: any): RulesetType => { + return { + rulesetTypeId: rulesetType.rulesetTypeId, + name: rulesetType.name, + lastUpdated: rulesetType.lastUpdated, + revisionFiles: rulesetType.revisionFiles + ? rulesetType.revisionFiles.filter((ruleset: any) => ruleset.deletedByUserId === null) + : [] + }; +}; + +export const rulesetTransformer = (ruleset: Prisma.RulesetGetPayload): Ruleset => { + const rulesWithTeams = ruleset.rules.filter((rule) => rule._count.teams > 0).length; + const totalRulesLength = ruleset.rules.length; + const teamsPercentage = totalRulesLength > 0 ? (rulesWithTeams / totalRulesLength) * 100 : 0; + + return { + fileId: ruleset.fileId, + rulesetId: ruleset.rulesetId, + name: ruleset.name, + dateCreated: ruleset.dateCreated, + active: ruleset.active, + assignedPercentage: teamsPercentage, + rulesetType: rulesetTypeTransformer(ruleset.rulesetType), + car: { + carId: ruleset.car.carId, + name: ruleset.car.wbsElement.name + }, + ruleAmount: totalRulesLength + }; +}; diff --git a/src/backend/src/utils/errors.utils.ts b/src/backend/src/utils/errors.utils.ts index f0de5d014a..e4efb57503 100644 --- a/src/backend/src/utils/errors.utils.ts +++ b/src/backend/src/utils/errors.utils.ts @@ -161,4 +161,11 @@ export type ExceptionObjectNames = | 'Index Code' | 'Reimbursement Product Other Reason' | 'Encryption Key' - | 'Reimbursement Request Comment'; + | 'Reimbursement Request Comment' + | 'Ruleset' + | 'Parent Rule' + | 'Referenced Rule' + | 'Rule' + | 'Project Rule' + | 'Ruleset Type' + | 'Active Ruleset for given Ruleset Type'; diff --git a/src/backend/src/utils/parse.utils.ts b/src/backend/src/utils/parse.utils.ts new file mode 100644 index 0000000000..8da38dc61f --- /dev/null +++ b/src/backend/src/utils/parse.utils.ts @@ -0,0 +1,399 @@ +import pdf from 'pdf-parse-new'; + +export interface ParsedRule { + ruleCode: string; + ruleContent: string; + parentRuleCode?: string; +} + +export const parseRulesFromPdf = async (buffer: Buffer, parserType: 'FSAE' | 'FHE'): Promise => { + const options = { + // max page number to parse, 0 = all pages + max: 0, + // errors: 0, warnings: 1, infos: 5 + verbosityLevel: 0 as const + }; + const pdfData = await pdf(buffer, options); + + if (parserType === 'FSAE') { + return parseFSAERules(pdfData.text); + } + if (parserType === 'FHE') { + return parseFHERules(pdfData.text); + } + throw new Error(`Invalid parser type: ${parserType}. Must be 'FSAE' or 'FHE'`); +}; + +/** + * Extracts lettered sub-rules from rule content (a, b, c, etc.) + * "EV.5.2 Main text a. Sub-rule" becomes: + * - EV.5.2 Main text + * - EV.5.2.a Sub-rule + * If no subrules exist, returns the original rule + * @param ruleCode parent rule code + * @param content rule content to extract from + * @returns array of parsed rules including main rule and any subrules + */ +const extractSubRules = (ruleCode: string, content: string): ParsedRule[] => { + const letterPattern = /\s+([a-z])\.\s+/g; + const matches = [...content.matchAll(letterPattern)]; + + if (matches.length === 0) { + // no subrules found, return original rule + return [ + { + ruleCode, + ruleContent: content.trim(), + parentRuleCode: findParentRuleCode(ruleCode) + } + ]; + } + const subRules: ParsedRule[] = []; + + // Extract the main rule content (everything before the first lettered item) + const firstMatchIndex = matches[0].index!; + const mainContent = content.substring(0, firstMatchIndex).trim(); + + // add main rule + subRules.push({ + ruleCode, + ruleContent: mainContent, + parentRuleCode: findParentRuleCode(ruleCode) + }); + + // Extract lettered sub-rules + for (let i = 0; i < matches.length; i++) { + const [, letter] = matches[i]; + const startIndex = matches[i].index! + matches[i][0].length; + + // Find where this sub-rule ends (either at next letter or end of rule content) + const endIndex = i < matches.length - 1 ? matches[i + 1].index! : content.length; + const subRuleContent = content.substring(startIndex, endIndex).trim(); + const subRuleCode = `${ruleCode}.${letter}`; + + subRules.push({ + ruleCode: subRuleCode, + ruleContent: subRuleContent, + parentRuleCode: ruleCode + }); + } + return subRules; +}; + +/** + * Determines parent rule code by removing last value. + * Top level rules return undefined. + * EV.5.2.2 -> EV.5.2 + * GR -> undefined + * @param ruleCode rule code to find a parent for + * @returns Parent rule code, or undefined if top level + */ +const findParentRuleCode = (ruleCode: string): string | undefined => { + const parts = ruleCode.split('.'); + if (parts.length <= 1) { + return undefined; + } + return parts.slice(0, -1).join('.'); +}; + +/** + * Updates rules with duplicate rule codes by appending .duplicate suffix + * and updates parent references to maintain parent-child relationships + * @param rules array of parsed rules + * @returns array of rules without duplicate rule codes and updated parent references + */ +const handleDuplicateCodes = (rules: ParsedRule[]): ParsedRule[] => { + const seenRuleCodes = new Map(); + const codeMapping = new Map(); // Maps original code to new code for duplicates + + // First pass: rename duplicates and track the mapping + const renamedRules = rules.map((rule) => { + const originalCode = rule.ruleCode; + + if (seenRuleCodes.has(originalCode)) { + // duplicate found + const count = seenRuleCodes.get(originalCode)!; + seenRuleCodes.set(originalCode, count + 1); + const suffix = count === 1 ? '.duplicate' : `.duplicate${count}`; + const newCode = `${originalCode}${suffix}`; + + // Track that this code was renamed + codeMapping.set(originalCode, newCode); + + return { + ...rule, + ruleCode: newCode + }; + } + seenRuleCodes.set(originalCode, 1); + return rule; + }); + + // Second pass: update parent references for rules whose parent was renamed + return renamedRules.map((rule) => { + if (rule.parentRuleCode && codeMapping.has(rule.parentRuleCode)) { + return { + ...rule, + parentRuleCode: codeMapping.get(rule.parentRuleCode) + }; + } + return rule; + }); +}; + +/**************** FSAE ****************/ + +const parseFSAERules = (text: string): ParsedRule[] => { + const rules: ParsedRule[] = []; + const lines = text.split('\n'); + + let currentRule: { code: string; text: string } | null = null; + + const saveCurrentRule = () => { + if (!currentRule) return; + const parsedRules = extractSubRules(currentRule.code, currentRule.text); + rules.push(...parsedRules); + }; + + for (const line of lines) { + const trimmedLine = line.trim(); + if (!trimmedLine) continue; + + // Skip page headers/footers + if (isHeaderFooterFSAE(trimmedLine)) { + continue; + } + + // Skip table of contents + if (/\.{4,}\s+\d+\s*$/.test(trimmedLine)) { + continue; + } + + // Check if this line starts a new rule + const rule = parseRuleNumberFSAE(trimmedLine); + if (rule) { + saveCurrentRule(); + currentRule = { + code: rule.ruleCode, + text: rule.ruleContent + }; + } else if (currentRule) { + currentRule.text += ' ' + trimmedLine; // else append to existing rule + } + } + saveCurrentRule(); + + const fixedRules = fixOrphanedRulesFSAE(rules); + return handleDuplicateCodes(fixedRules); +}; + +/** + * Determines if this line starts a new rule, if so extracts code and content of the rule + * Matches rule pattern (e.g. GR.1.1 some text) or section pattern (e.g. GR - TEXT) + * @param line single line in the extracted text from the ruleset pdf + * @returns rule code and content, or null if this line does not start a new rule + */ +const parseRuleNumberFSAE = (line: string): ParsedRule | null => { + // Match rule patterns like "GR.1.1" followed by text + const rulePattern = /^([A-Z]{1,4}(?:\.[\d]+)+)\s+(.+)$/; + // Match section patterns like "GR - GENERAL REGULATIONS or PS - PRE-COMPETITION SUBMISSIONS" + const sectionPattern = /^([A-Z]{1,4})\s*-\s*(.+)$/; + + const match = line.match(rulePattern) || line.match(sectionPattern); + if (match) { + const cleanContent = match[2].replace(/\.{5,}/g, '.....'); + return { + ruleCode: match[1], + ruleContent: cleanContent + }; + } + return null; +}; + +/** + * Checks if a line is a page header/footer that should be skipped + * @param line line to check + * @returns true if line should be skipped + */ +const isHeaderFooterFSAE = (line: string): boolean => { + const trimmed = line.trim(); + + // Match FSAE headers like "Formula SAE® Rules 2025 © 2024 SAE International Page 7 of 143 Version 1.0 31 Aug 2024" + if (/Formula SAE.*Rules.*\d{4}.*SAE International.*Page \d+ of \d+/i.test(trimmed)) { + return true; + } + // Match standalone page numbers + if (/^Page \d+ of \d+$/i.test(trimmed)) { + return true; + } + // Match version strings + if (/^Version \d+\.\d+.*\d{1,2}\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{4}$/i.test(trimmed)) { + return true; + } + + return false; +}; + +/** + * Updates rules to point to nearest existing parent if their assigned parent doesn't exist. + * D.8.1.2 -> checks for D.8.1, if missing goes to D.8, then D + * @param rules array of parsed rules + * @returns rules with corrected parent references + */ +const fixOrphanedRulesFSAE = (rules: ParsedRule[]): ParsedRule[] => { + const existingCodes = new Set(rules.map((r) => r.ruleCode)); + + return rules.map((rule) => { + // skip if no parent or parent exists + if (!rule.parentRuleCode || existingCodes.has(rule.parentRuleCode)) { + return rule; // Top-level rule + } + + // Set parent doesn't exist, walk up the hierarchy + const parts = rule.ruleCode.split('.'); + for (let i = parts.length - 2; i > 0; i--) { + const ancestorCode = parts.slice(0, i).join('.'); + if (existingCodes.has(ancestorCode)) { + return { ...rule, parentRuleCode: ancestorCode }; + } + } + + // No ancestor exists, becomes top-level + return { ...rule, parentRuleCode: undefined }; + }); +}; + +/**************** FHE *****************/ + +const parseFHERules = (text: string): ParsedRule[] => { + const rules: ParsedRule[] = []; + const lines = text.split('\n'); + let inRulesSection = false; + let currentRule: { code: string; text: string } | null = null; + + const saveCurrentRule = () => { + if (!currentRule) return; + const parsedRules = extractSubRules(currentRule.code, currentRule.text); + rules.push(...parsedRules); + }; + + for (const line of lines) { + const trimmedLine = line.trim(); + if (!trimmedLine) continue; + if (/^Index of Tables/i.test(trimmedLine)) { + inRulesSection = true; + } + // Skip table of contents + if (inRulesSection) { + if (/^2025 Formula Hybrid.*Rules/i.test(trimmedLine)) { + saveCurrentRule(); + currentRule = null; + continue; + } + + // Check if this line starts a new rule + const rule = parseRuleNumberFHE(trimmedLine); + if (rule) { + saveCurrentRule(); + currentRule = { + code: rule.ruleCode, + text: rule.ruleContent + }; + } else if (currentRule) { + // Append to existing rule + currentRule.text += ' ' + trimmedLine; + } + } + } + saveCurrentRule(); + + const fixedRules = fixOrphanedRulesFHE(rules); + return handleDuplicateCodes(fixedRules); +}; + +/** + * Determines if this line starts a new rule, if so extracts code and content of the rule + * Matches three patterns: rule ("1T3.17.1 Text"), part ("PART A1 - Text"), and article ("ARTICLE A1 Text") + * @param line single line in the extracted text from the ruleset pdf + * @returns rule code and content, or null if this line does not start a new rule + */ +const parseRuleNumberFHE = (line: string): ParsedRule | null => { + // Match FHE rule patterns like "1T3.17.1" followed by text + const rulePattern = /^(\d+[A-Z]+\d+(?:\.\d+)*)\s+(.+)$/; + + // "PART A1 - ADMINISTRATIVE REGULATIONS" removes "PART" and captures "A1" as rule code, rest as content + const partMatch = line.match(/^PART\s+([A-Z0-9]+)\s+-\s+(.+)$/); + if (partMatch) { + return { + ruleCode: partMatch[1], // "A1", not "PART A1" + ruleContent: partMatch[2] + }; + } + + // "ARTICLE A1 FORMULA HYBRID + ELECTRIC OVERVIEW" + // Captures "A1" as rule code, removes "ARTICLE" and adds rest as content + const articleMatch = line.match(/^ARTICLE\s+([A-Z]+\d+)\s+(.+)$/); + if (articleMatch) { + return { + ruleCode: articleMatch[1], // "A11", not "ARTICLE A11" + ruleContent: articleMatch[2] + }; + } + + const match = line.match(rulePattern); + if (match) { + return { + ruleCode: match[1], + ruleContent: match[2] + }; + } + + return null; +}; + +/** + * Updates rules to point to nearest existing parent if their assigned parent doesn't exist. + * D.8.1.2 -> checks for D.8.1, if missing goes to D.8, then D + * Also for FHE formatting 1A11.1 -> checks for 1A11, if missing tries A11 (article format) + * @param rules array of parsed rules + * @returns rules with corrected parent references + */ +const fixOrphanedRulesFHE = (rules: ParsedRule[]): ParsedRule[] => { + const existingCodes = new Set(rules.map((r) => r.ruleCode)); + + return rules.map((rule) => { + // skip if no parent or parent exists + if (!rule.parentRuleCode || existingCodes.has(rule.parentRuleCode)) { + return rule; + } + + // Set parent doesn't exist, walk up the hierarchy + const parts = rule.ruleCode.split('.'); + for (let i = parts.length - 2; i > 0; i--) { + const ancestorCode = parts.slice(0, i).join('.'); + + if (existingCodes.has(ancestorCode)) { + return { ...rule, parentRuleCode: ancestorCode }; + } + + // Also check stripped version (1A5 -> A5) + if (/^\d+[A-Z]+/.test(ancestorCode)) { + const strippedAncestor = ancestorCode.replace(/^\d+/, ''); + if (existingCodes.has(strippedAncestor)) { + return { ...rule, parentRuleCode: strippedAncestor }; + } + } + } + + // Special case: if parent is like "1A11" and doesn't exist, try "A11" (article format) + // This handles rules like "1A11.1" whose parent "1A11" doesn't exist but should be "A11" + if (rule.parentRuleCode && /^\d+[A-Z]+\d+$/.test(rule.parentRuleCode)) { + const withoutLeadingDigit = rule.parentRuleCode.substring(1); // "1A11" -> "A11" + if (existingCodes.has(withoutLeadingDigit)) { + return { ...rule, parentRuleCode: withoutLeadingDigit }; + } + } + + return { ...rule, parentRuleCode: undefined }; + }); +}; diff --git a/src/backend/tests/test-utils.ts b/src/backend/tests/test-utils.ts index 5f69cc5f78..a03c1718bd 100644 --- a/src/backend/tests/test-utils.ts +++ b/src/backend/tests/test-utils.ts @@ -115,6 +115,11 @@ export const resetUsers = async () => { await prisma.part_Review.deleteMany(); await prisma.part_Submission.deleteMany(); await prisma.part.deleteMany(); + await prisma.rule_Status_Change.deleteMany(); + await prisma.project_Rule.deleteMany(); + await prisma.rule.deleteMany(); + await prisma.ruleset.deleteMany(); + await prisma.ruleset_Type.deleteMany(); await prisma.project.deleteMany(); await prisma.frequentlyAskedQuestion.deleteMany(); await prisma.material.deleteMany(); @@ -130,6 +135,9 @@ export const resetUsers = async () => { await prisma.reimbursement_Request.deleteMany(); await prisma.vendor.deleteMany(); await prisma.account_Code.deleteMany(); + await prisma.rule.deleteMany(); + await prisma.ruleset.deleteMany(); + await prisma.ruleset_Type.deleteMany(); await prisma.car.deleteMany(); await prisma.task.deleteMany(); await prisma.stage_Gate_CR.deleteMany(); diff --git a/src/backend/tests/unit/rule.test.ts b/src/backend/tests/unit/rule.test.ts new file mode 100644 index 0000000000..3304562f7a --- /dev/null +++ b/src/backend/tests/unit/rule.test.ts @@ -0,0 +1,1953 @@ +import RulesService from '../../src/services/rules.services'; +import { Organization, User, Project, Car, Ruleset_Type, Ruleset, Rule_Completion, Team } from '@prisma/client'; +import { + supermanAdmin, + financeMember, + wonderwomanGuest, + batmanAppAdmin, + aquamanLeadership, + alfred, + flashAdmin +} from '../test-data/users.test-data'; +import { + createTestOrganization, + createTestProject, + createTestUser, + resetUsers, + createTestTeam, + createTestTeamType +} from '../test-utils'; +import prisma from '../../src/prisma/prisma'; +import { + AccessDeniedException, + AccessDeniedGuestException, + DeletedException, + HttpException, + NotFoundException, + AccessDeniedAdminOnlyException, + InvalidOrganizationException +} from '../../src/utils/errors.utils'; +import TeamsService from '../../src/services/teams.services'; + +describe('Create Rules Tests', () => { + let orgId: string; + let organization: Organization; + let batman: User; + let superman: User; + let aquaman: User; + let wonderwoman: User; + let rulesetId: string; + let carId: string; + let rulesetType: Ruleset_Type; + + beforeEach(async () => { + organization = await createTestOrganization(); + orgId = organization.organizationId; + batman = await createTestUser(batmanAppAdmin, orgId); + superman = await createTestUser(supermanAdmin, orgId); + aquaman = await createTestUser(aquamanLeadership, orgId); + wonderwoman = await createTestUser(wonderwomanGuest, orgId); + + const car = await prisma.car.create({ + data: { + wbsElement: { + create: { + name: 'Test Car', + carNumber: 0, + projectNumber: 0, + workPackageNumber: 0, + organizationId: orgId + } + } + } + }); + ({ carId } = car); + + rulesetType = await prisma.ruleset_Type.create({ + data: { + name: 'FSAE Rules', + createdBy: { connect: { userId: batman.userId } }, + organization: { connect: { organizationId: organization.organizationId } } + } + }); + + const ruleset1 = await prisma.ruleset.create({ + data: { + fileId: 'test-file-id', + name: '2025 FSAE Rules', + active: true, + rulesetType: { connect: { rulesetTypeId: rulesetType.rulesetTypeId } }, + car: { connect: { carId } }, + createdBy: { connect: { userId: batman.userId } }, + dateCreated: new Date('2025-01-01T10:00:00Z') + } + }); + + ({ rulesetId } = ruleset1); + }); + + afterEach(async () => { + await resetUsers(); + }); + + describe('Create Rule', () => { + it('successfully creates a basic rule', async () => { + const rule = await RulesService.createRule( + batman, + 'T.1.1.1', + 'The vehicle must have four wheels', + rulesetId, + organization + ); + + expect(rule.ruleCode).toBe('T.1.1.1'); + expect(rule.ruleContent).toBe('The vehicle must have four wheels'); + expect(rule.parentRule).toBeUndefined(); + expect(rule.subRuleIds).toHaveLength(0); + expect(rule.referencedRuleIds).toHaveLength(0); + expect(rule.imageFileIds).toHaveLength(0); + }); + + it('successfully creates a rule with a parent', async () => { + const parentRule = await RulesService.createRule(batman, 'T.1.1', 'Vehicle Requirements', rulesetId, organization); + + const childRule = await RulesService.createRule( + superman, + 'T.1.1.1', + 'The vehicle must have four wheels', + rulesetId, + organization, + parentRule.ruleId + ); + + expect(childRule.parentRule?.ruleId).toBe(parentRule.ruleId); + expect(childRule.ruleCode).toBe('T.1.1.1'); + }); + + it('successfully creates a rule with referenced rules', async () => { + const rule1 = await RulesService.createRule(batman, 'T.1.1', 'Vehicle must have wheels', rulesetId, organization); + + const rule2 = await RulesService.createRule(batman, 'T.1.2', 'Vehicle must have brakes', rulesetId, organization); + + const rule3 = await RulesService.createRule( + superman, + 'T.2.1', + 'Braking system must work with wheels (see T.1.1 and T.1.2)', + rulesetId, + organization, + undefined, + [rule1.ruleId, rule2.ruleId] + ); + + expect(rule3.referencedRuleIds).toHaveLength(2); + expect(rule3.referencedRuleIds).toContain(rule1.ruleId); + expect(rule3.referencedRuleIds).toContain(rule2.ruleId); + }); + + it('successfully creates a rule with image file IDs', async () => { + const rule = await RulesService.createRule( + batman, + 'T.3.1', + 'Chassis must meet specifications', + rulesetId, + organization, + undefined, + [], + ['file-id-1', 'file-id-2'] + ); + + expect(rule.imageFileIds).toHaveLength(2); + expect(rule.imageFileIds).toContain('file-id-1'); + expect(rule.imageFileIds).toContain('file-id-2'); + }); + + it('fails when guest tries to create a rule', async () => { + await expect(RulesService.createRule(wonderwoman, 'T.1.1', 'Some rule', rulesetId, organization)).rejects.toThrow( + new AccessDeniedException('Only members and above can create rules') + ); + }); + + it('fails when ruleset does not exist', async () => { + await expect(RulesService.createRule(batman, 'T.1.1', 'Some rule', 'fake-ruleset-id', organization)).rejects.toThrow( + new NotFoundException('Ruleset', 'fake-ruleset-id') + ); + }); + + it('fails when ruleset is deleted', async () => { + await prisma.ruleset.update({ + where: { rulesetId }, + data: { deletedBy: { connect: { userId: batman.userId } } } + }); + + await expect(RulesService.createRule(batman, 'T.1.1', 'Some rule', rulesetId, organization)).rejects.toThrow( + new DeletedException('Ruleset', rulesetId) + ); + }); + + it('fails when duplicate rule code in same ruleset', async () => { + await RulesService.createRule(batman, 'T.1.1', 'First rule', rulesetId, organization); + + await expect(RulesService.createRule(superman, 'T.1.1', 'Duplicate code', rulesetId, organization)).rejects.toThrow( + new HttpException(400, 'Rule with code T.1.1 already exists in this ruleset') + ); + }); + + it('fails when parent rule does not exist', async () => { + await expect( + RulesService.createRule(batman, 'T.1.1', 'Some rule', rulesetId, organization, 'fake-parent-id') + ).rejects.toThrow(new NotFoundException('Parent Rule', 'fake-parent-id')); + }); + + it('fails when parent rule is deleted', async () => { + const parentRule = await RulesService.createRule(batman, 'T.1', 'Parent', rulesetId, organization); + + await prisma.rule.update({ + where: { ruleId: parentRule.ruleId }, + data: { dateDeleted: new Date(), deletedBy: { connect: { userId: batman.userId } } } + }); + + await expect( + RulesService.createRule(superman, 'T.1.1', 'Child', rulesetId, organization, parentRule.ruleId) + ).rejects.toThrow(new DeletedException('Parent Rule', parentRule.ruleId)); + }); + + it('fails when parent rule is in different ruleset', async () => { + const otherRuleset = await prisma.ruleset.create({ + data: { + fileId: 'other-file', + name: 'Other Rules', + active: true, + rulesetTypeId: (await prisma.ruleset_Type.findFirst())!.rulesetTypeId, + carId, + createdByUserId: batman.userId + } + }); + + const parentRule = await RulesService.createRule(batman, 'T.1', 'Parent', otherRuleset.rulesetId, organization); + + await expect( + RulesService.createRule(superman, 'T.1.1', 'Child', rulesetId, organization, parentRule.ruleId) + ).rejects.toThrow(new HttpException(400, 'Parent rule must be in the same ruleset')); + }); + + it('fails when referenced rule does not exist', async () => { + await expect( + RulesService.createRule(batman, 'T.1.1', 'Some rule', rulesetId, organization, undefined, ['fake-rule-id']) + ).rejects.toThrow(new NotFoundException('Referenced Rule', 'provided IDs')); + }); + + it('fails when referenced rule is deleted', async () => { + const rule1 = await RulesService.createRule(batman, 'T.1.1', 'Rule 1', rulesetId, organization); + + await prisma.rule.update({ + where: { ruleId: rule1.ruleId }, + data: { dateDeleted: new Date(), deletedBy: { connect: { userId: batman.userId } } } + }); + + await expect( + RulesService.createRule(superman, 'T.1.2', 'Rule 2', rulesetId, organization, undefined, [rule1.ruleId]) + ).rejects.toThrow(new DeletedException('Referenced Rule', rule1.ruleId)); + }); + + it('allows members and above to create rules', async () => { + await RulesService.createRule(aquaman, 'T.1.1', 'Member created rule', rulesetId, organization); + await RulesService.createRule(aquaman, 'T.1.2', 'Leadership created rule', rulesetId, organization); + await RulesService.createRule(superman, 'T.1.3', 'Admin created rule', rulesetId, organization); + }); + + describe('Create ruleset', () => { + it('successful create ruleset', async () => { + const ruleset = await RulesService.createRuleset( + superman, + organization, + 'ruleset name', + rulesetType.rulesetTypeId, + 0, + false, + 'fileId' + ); + + expect(ruleset.name).toEqual('ruleset name'); + }); + it('Create ruleset fails when submitters is not leadership', async () => { + await expect( + async () => + await RulesService.createRuleset( + wonderwoman, + organization, + 'ruleset name', + rulesetType.rulesetTypeId, + 0, + false, + 'fileId' + ) + ).rejects.toThrow(new AccessDeniedException('only leadership and above can create ruleset!')); + }); + it('Create ruleset fails when given bad ruleset id', async () => { + await expect( + async () => + await RulesService.createRuleset(superman, organization, 'ruleset name', 'bad ruleset type', 0, false, 'fileId') + ).rejects.toThrow(new NotFoundException('Ruleset Type', 'bad ruleset type')); + }); + it('Create ruleset fails when given bad car number', async () => { + await expect( + async () => + await RulesService.createRuleset( + superman, + organization, + 'ruleset name', + rulesetType.rulesetTypeId, + 12312312, + false, + 'fileId' + ) + ).rejects.toThrow(new NotFoundException('Car', 12312312)); + }); + }); + }); + + describe('Complex Rule Scenarios', () => { + it('creates a hierarchical rule structure', async () => { + const root = await RulesService.createRule(batman, 'T.1', 'Technical Rules', rulesetId, organization); + + const child1 = await RulesService.createRule( + batman, + 'T.1.1', + 'Vehicle Requirements', + rulesetId, + organization, + root.ruleId + ); + + const child2 = await RulesService.createRule( + batman, + 'T.1.2', + 'Safety Requirements', + rulesetId, + organization, + root.ruleId + ); + + const grandchild1 = await RulesService.createRule( + superman, + 'T.1.1.1', + 'Wheels', + rulesetId, + organization, + child1.ruleId + ); + + expect(root.parentRule).toBeUndefined(); + expect(child1.parentRule?.ruleId).toBe(root.ruleId); + expect(child2.parentRule?.ruleId).toBe(root.ruleId); + expect(grandchild1.parentRule?.ruleId).toBe(child1.ruleId); + }); + + it('creates rules with cross-references', async () => { + const wheelRule = await RulesService.createRule(batman, 'T.1.1', 'Wheel specifications', rulesetId, organization); + + const brakeRule = await RulesService.createRule(batman, 'T.1.2', 'Brake specifications', rulesetId, organization); + + const brakingSystemRule = await RulesService.createRule( + superman, + 'T.2.1', + 'Braking system must comply with T.1.1 and T.1.2', + rulesetId, + organization, + undefined, + [wheelRule.ruleId, brakeRule.ruleId] + ); + + const wheelRuleFromDb = await prisma.rule.findUnique({ + where: { ruleId: wheelRule.ruleId }, + include: { referencedBy: true } + }); + + expect(wheelRuleFromDb?.referencedBy.some((r) => r.ruleId === brakingSystemRule.ruleId)).toBe(true); + }); + }); + + describe('Get rulesets by ruleset type', () => { + it('Successful get rulesets by ruleset types', async () => { + const rulesets = await RulesService.getRulesetsByRulesetType(rulesetType.rulesetTypeId, orgId); + expect(rulesets.length).toBe(1); + expect(rulesets[0].name).toBe('2025 FSAE Rules'); + expect(rulesets[0].active).toBeTruthy(); + expect(rulesets[0].assignedPercentage).toBe(0); + }); + + it('Successful get rulesets by ruleset types after deleting ruleset', async () => { + // Deactivate the ruleset before deleting + await prisma.ruleset.update({ + where: { rulesetId }, + data: { active: false } + }); + + await RulesService.deleteRuleset(rulesetId, batman.userId, orgId); + const rulesets = await RulesService.getRulesetsByRulesetType(rulesetType.rulesetTypeId, orgId); + expect(rulesets.length).toBe(0); + }); + + it('Successful get rulesets by ruleset types after adding ruleset', async () => { + await prisma.ruleset.create({ + data: { + fileId: 'test-file-id2', + name: '2025 FSAE Rules2', + active: true, + rulesetType: { connect: { rulesetTypeId: rulesetType.rulesetTypeId } }, + car: { connect: { carId } }, + createdBy: { connect: { userId: batman.userId } } + } + }); + const rulesets = await RulesService.getRulesetsByRulesetType(rulesetType.rulesetTypeId, orgId); + expect(rulesets.length).toBe(2); + expect(rulesets[0].name).toBe('2025 FSAE Rules2'); + expect(rulesets[1].name).toBe('2025 FSAE Rules'); + }); + }); + + describe('Get Child Rules', () => { + it('Successfully gets child rules for a parent rule', async () => { + const parentRule = await RulesService.createRule(batman, 'T.1', 'Parent Rule', rulesetId, organization); + await RulesService.createRule(batman, 'T.1.1', 'Child Rule 1', rulesetId, organization, parentRule.ruleId); + await RulesService.createRule(batman, 'T.1.2', 'Child Rule 2', rulesetId, organization, parentRule.ruleId); + const childRules = await RulesService.getChildRules(parentRule.ruleId, organization); + expect(childRules.length).toBe(2); + expect(childRules[0].ruleCode).toBe('T.1.1'); + expect(childRules[1].ruleCode).toBe('T.1.2'); + }); + + it('Successfully gets child rules after deleting child rule', async () => { + const parentRule = await RulesService.createRule(batman, 'T.2', 'Parent Rule', rulesetId, organization); + const childRule = await RulesService.createRule( + batman, + 'T.2.1', + 'Child Rule', + rulesetId, + organization, + parentRule.ruleId + ); + await RulesService.deleteRule(childRule.ruleId, batman, organization); + const childRules = await RulesService.getChildRules(parentRule.ruleId, organization); + expect(childRules.length).toBe(0); + }); + + it('Successfully gets child rules after adding child rule', async () => { + const parentRule = await RulesService.createRule(batman, 'T.3', 'Parent Rule', rulesetId, organization); + await RulesService.createRule(batman, 'T.3.1', 'Child Rule 1', rulesetId, organization, parentRule.ruleId); + const childRulesAfterOne = await RulesService.getChildRules(parentRule.ruleId, organization); + expect(childRulesAfterOne.length).toBe(1); + await RulesService.createRule(batman, 'T.3.2', 'Child Rule 2', rulesetId, organization, parentRule.ruleId); + const childRulesAfterTwo = await RulesService.getChildRules(parentRule.ruleId, organization); + expect(childRulesAfterTwo.length).toBe(2); + expect(childRulesAfterTwo[0].ruleCode).toBe('T.3.1'); + expect(childRulesAfterTwo[1].ruleCode).toBe('T.3.2'); + }); + + it('Fails if parent rule does not exist', async () => { + await expect(async () => await RulesService.getChildRules('fake-rule-id', organization)).rejects.toThrow( + new NotFoundException('Rule', 'fake-rule-id') + ); + }); + + it('Fails if parent rule is deleted', async () => { + const parentRule = await RulesService.createRule(batman, 'T.4', 'Parent Rule', rulesetId, organization); + await RulesService.deleteRule(parentRule.ruleId, batman, organization); + await expect(async () => await RulesService.getChildRules(parentRule.ruleId, organization)).rejects.toThrow( + new DeletedException('Rule', parentRule.ruleId) + ); + }); + + it('Fails if parent rule is from another organization', async () => { + //manually create a user to avoid same googleAuthID as otherBatman + const otherUser = await prisma.user.create({ + data: { + firstName: alfred.firstName, + lastName: alfred.lastName, + email: alfred.email, + googleAuthId: alfred.googleAuthId + } + }); + const otherOrganization = await prisma.organization.create({ + data: { + name: 'Other Org', + description: 'Another organization', + applicationLink: '', + userCreated: { + connect: { + userId: otherUser.userId + } + } + } + }); + const otherBatman = await createTestUser(flashAdmin, otherOrganization.organizationId); + const otherCar = await prisma.car.create({ + data: { + wbsElement: { + create: { + name: 'Other Car', + carNumber: 2, + projectNumber: 0, + workPackageNumber: 0, + organizationId: otherOrganization.organizationId + } + } + }, + include: { wbsElement: true } + }); + + const otherRuleset = await prisma.ruleset.create({ + data: { + name: 'Other Ruleset', + fileId: 'other', + active: true, + carId: otherCar.carId, + createdByUserId: otherBatman.userId, + rulesetTypeId: rulesetType.rulesetTypeId + } + }); + const otherParentRule = await prisma.rule.create({ + data: { + ruleCode: 'O.1', + ruleContent: 'Other Parent', + imageFileIds: [], + rulesetId: otherRuleset.rulesetId, + createdByUserId: otherBatman.userId + } + }); + await expect(async () => await RulesService.getChildRules(otherParentRule.ruleId, organization)).rejects.toThrow( + new InvalidOrganizationException('Rule') + ); + }); + }); + describe('Update ruleset status', () => { + it('update ruleset status - successful', async () => { + const ruleset1 = await RulesService.updateRuleset(batman, orgId, rulesetId, 'name1', false); + expect(ruleset1.active).toBe(false); + expect(ruleset1.name).toBe('name1'); + const ruleset2 = await RulesService.updateRuleset(batman, orgId, rulesetId, 'name2', true); + expect(ruleset2.active).toBe(true); + expect(ruleset2.name).toBe('name2'); + }); + it('update ruleset status on deleted ruleset fails', async () => { + // Deactivate the ruleset before deleting + await prisma.ruleset.update({ + where: { rulesetId }, + data: { active: false } + }); + await RulesService.deleteRuleset(rulesetId, batman.userId, orgId); + await expect(async () => await RulesService.updateRuleset(batman, orgId, rulesetId, 'name', false)).rejects.toThrow( + new NotFoundException('Ruleset', rulesetId) + ); + }); + it('update active ruleset successful with active ruleset in different type', async () => { + const ruleset2 = await RulesService.createRuleset( + superman, + organization, + 'ruleset name', + (await RulesService.createRulesetType(batman, 'ruleset type 2', organization)).rulesetTypeId, + 0, + false, + 'fileId' + ); + await RulesService.updateRuleset(batman, orgId, ruleset2.rulesetId, 'name', false); + const ruleset = await RulesService.updateRuleset(batman, orgId, rulesetId, 'name', true); + expect(ruleset.active).toBe(true); + }); + it('update ruleset status fails with wrong org', async () => { + const wrongOrg = await prisma.organization.create({ + data: { + name: 'wrong org', + userCreatedId: batman.userId, + description: 'desc', + applyInterestImageId: '1', + exploreAsGuestImageId: '1', + applicationLink: '1' + } + }); + + const wrongOrgCar = await prisma.car.create({ + data: { + wbsElement: { + create: { + name: 'wrong org car', + carNumber: 0, + projectNumber: 0, + workPackageNumber: 0, + organizationId: wrongOrg.organizationId + } + } + } + }); + + const wrongOrgRulesetType = await prisma.ruleset_Type.create({ + data: { + name: 'ruleset type 2', + createdBy: { connect: { userId: batman.userId } }, + organization: { connect: { organizationId: wrongOrg.organizationId } } + } + }); + + const wrongOrgRuleset = await prisma.ruleset.create({ + data: { + fileId: 'fileId', + name: 'ruleset name', + active: false, + rulesetType: { connect: { rulesetTypeId: wrongOrgRulesetType.rulesetTypeId } }, + car: { connect: { carId: wrongOrgCar.carId } }, + createdBy: { connect: { userId: batman.userId } } + } + }); + + await expect( + async () => await RulesService.updateRuleset(batman, orgId, wrongOrgRuleset.rulesetId, 'name', false) + ).rejects.toThrow(new NotFoundException('Ruleset', wrongOrgRuleset.rulesetId)); + }); + it('update ruleset status - fails non leadership', async () => { + await expect( + async () => await RulesService.updateRuleset(wonderwoman, orgId, rulesetId, 'name', false) + ).rejects.toThrow(new AccessDeniedException('You do not have permissions to update ruleset status')); + }); + it('update ruleset status - fails if one is already active in same type', async () => { + const ruleset2 = await RulesService.createRuleset( + superman, + organization, + 'ruleset name', + rulesetType.rulesetTypeId, + 0, + false, + 'fileId' + ); + await expect( + async () => await RulesService.updateRuleset(batman, orgId, ruleset2.rulesetId, 'name', true) + ).rejects.toThrow(new HttpException(400, 'There is already an active ruleset for this ruleset type')); + }); + }); +}); + +describe('Rule Tests', () => { + let organization: Organization; + let orgId: string; + let otherOrg: Organization; + let admin: User; + let nonLeadership: User; + let guest: User; + let project: Project; + let fsaeRulesetType: Ruleset_Type; + let emptyRulesetType: Ruleset_Type; + let testTeam: Team; + + beforeEach(async () => { + organization = await createTestOrganization(); + orgId = organization.organizationId; + admin = await createTestUser(supermanAdmin, organization.organizationId); + nonLeadership = await createTestUser(financeMember, organization.organizationId); + guest = await createTestUser(wonderwomanGuest, organization.organizationId); + project = await createTestProject(admin, organization.organizationId); + testTeam = await prisma.team.create({ + data: { + teamName: 'Test', + slackId: 'test-slack', + headId: admin.userId, + organizationId: organization.organizationId + } + }); + const otherOrgUser = await prisma.user.create({ + data: { + firstName: 'Other', + lastName: 'Admin', + email: 'other@test.com', + googleAuthId: 'otherOrganizationCreator' // different googleAuthId + } + }); + otherOrg = await prisma.organization.create({ + data: { + name: 'Other Organization', + description: 'Other test organization', + applicationLink: '', + userCreated: { + connect: { + userId: otherOrgUser.userId + } + } + } + }); + + fsaeRulesetType = await prisma.ruleset_Type.create({ + data: { + name: 'FSAE', + createdBy: { connect: { userId: admin.userId } }, + organization: { connect: { organizationId: organization.organizationId } } + } + }); + + emptyRulesetType = await prisma.ruleset_Type.create({ + data: { + name: 'Ruleset Type with no Active Rulesets or Anything', + createdBy: { connect: { userId: admin.userId } }, + organization: { connect: { organizationId: organization.organizationId } } + } + }); + }); + + afterEach(async () => { + await resetUsers(); + }); + + let carCounter = 1; + const createUniqueCar = async (orgId: string) => { + const car = await prisma.car.create({ + data: { + wbsElement: { + create: { + name: `Test Car ${carCounter}`, + carNumber: carCounter, + projectNumber: 0, + workPackageNumber: 0, + organizationId: orgId + } + } + }, + include: { + wbsElement: true + } + }); + carCounter++; + return car; + }; + + const setupRules = async (car: Car) => { + const ruleset1 = await prisma.ruleset.create({ + data: { + name: 'FSAE Rules 2025', + fileId: 'fsae-rules-2025', + active: true, + dateCreated: new Date(), + car: { connect: { carId: car.carId } }, + createdBy: { connect: { userId: admin.userId } }, + rulesetType: { connect: { rulesetTypeId: fsaeRulesetType.rulesetTypeId } } + } + }); + + const ruleset2 = await prisma.ruleset.create({ + data: { + fileId: 'test-file-id', + name: 'Inactive 2025 FSAE Rules', + active: false, + rulesetType: { connect: { rulesetTypeId: fsaeRulesetType.rulesetTypeId } }, + car: { connect: { carId: car.carId } }, + createdBy: { connect: { userId: admin.userId } }, + dateCreated: new Date('2024-12-31T10:00:00Z') + } + }); + + const topLevelRule = await prisma.rule.create({ + data: { + ruleCode: 'T', + ruleContent: 'PART T - GENERAL TECHNICAL REQUIREMENTS', + imageFileIds: [], + dateCreated: new Date(), + ruleset: { connect: { rulesetId: ruleset1.rulesetId } }, + createdBy: { connect: { userId: admin.userId } } + } + }); + + const leafRule1 = await prisma.rule.create({ + data: { + ruleCode: 'T2', + ruleContent: 'The vehicle must be open-wheeled and open-cockpit...', + imageFileIds: [], + dateCreated: new Date(), + ruleset: { connect: { rulesetId: ruleset1.rulesetId } }, + createdBy: { connect: { userId: admin.userId } }, + parentRule: { connect: { ruleId: topLevelRule.ruleId } } + } + }); + + const leafRule2 = await prisma.rule.create({ + data: { + ruleCode: 'T2.1', + ruleContent: 'T2.1 Vehicle Configuration', + imageFileIds: [], + dateCreated: new Date(), + ruleset: { connect: { rulesetId: ruleset1.rulesetId } }, + createdBy: { connect: { userId: admin.userId } }, + parentRule: { connect: { ruleId: topLevelRule.ruleId } } + } + }); + + return { ruleset1, ruleset2, topLevelRule, leafRule1, leafRule2 }; + }; + + describe('Create Ruleset Type', () => { + it('Fails if user is not leadership or above', async () => { + await expect(async () => await RulesService.createRulesetType(guest, 'FSAE', organization)).rejects.toThrow( + new AccessDeniedException('only leadership and above can create ruleset types!') + ); + }); + + it('Succeeds and creates a ruleset type', async () => { + const result = await RulesService.createRulesetType(await createTestUser(batmanAppAdmin, orgId), 'FSAE', organization); + + expect(result.name).toEqual('FSAE'); + }); + }); + + describe('Project Rule endpoints', () => { + it('Creates a project rule successfully', async () => { + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + const projectRule = await RulesService.createProjectRule(admin, organization, topLevelRule.ruleId, project.projectId); + + expect(projectRule.projectRuleId).toBeDefined(); + expect(projectRule.rule).toBeDefined(); + expect(projectRule.rule.ruleId).toBe(topLevelRule.ruleId); + expect(projectRule.rule.ruleCode).toBe(topLevelRule.ruleCode); + expect(projectRule.projectId).toBe(project.projectId); + expect(projectRule.statusHistory).toEqual([]); + expect(projectRule.currentStatus).toBe(Rule_Completion.REVIEW); + }); + it('Creates a project rule successfully for a leaf rule', async () => { + const car = await createUniqueCar(orgId); + const { leafRule1 } = await setupRules(car); + const projectRule = await RulesService.createProjectRule(admin, organization, leafRule1.ruleId, project.projectId); + + expect(projectRule.projectRuleId).toBeDefined(); + expect(projectRule.rule).toBeDefined(); + expect(projectRule.rule.ruleId).toBe(leafRule1.ruleId); + expect(projectRule.rule.ruleCode).toBe(leafRule1.ruleCode); + expect(projectRule.projectId).toBe(project.projectId); + expect(projectRule.statusHistory).toEqual([]); + expect(projectRule.currentStatus).toBe(Rule_Completion.REVIEW); + }); + it('Create project rule fails if user does not have permission', async () => { + const car = await createUniqueCar(orgId); + const { leafRule1 } = await setupRules(car); + await expect( + async () => await RulesService.createProjectRule(nonLeadership, organization, leafRule1.ruleId, project.projectId) + ).rejects.toThrow(new AccessDeniedException('You do not have permissions to assign rules to projects')); + }); + it('Create project rule fails if rule was deleted', async () => { + const car = await createUniqueCar(orgId); + const { leafRule2 } = await setupRules(car); + + await prisma.rule.update({ + where: { ruleId: leafRule2.ruleId }, + data: { dateDeleted: new Date() } + }); + await expect( + async () => await RulesService.createProjectRule(admin, organization, leafRule2.ruleId, project.projectId) + ).rejects.toThrow(new DeletedException('Rule', leafRule2.ruleId)); + }); + it('Create project rule fails if rule does not exist', async () => { + await expect( + async () => await RulesService.createProjectRule(admin, organization, '019263825673825738', project.projectId) + ).rejects.toThrow(new NotFoundException('Rule', '019263825673825738')); + }); + it('Create project rule fails if project was deleted', async () => { + const car = await createUniqueCar(orgId); + const { leafRule2 } = await setupRules(car); + await prisma.project.update({ + where: { projectId: project.projectId }, + data: { + wbsElement: { + update: { dateDeleted: new Date() } + } + } + }); + await expect( + async () => await RulesService.createProjectRule(admin, organization, leafRule2.ruleId, project.projectId) + ).rejects.toThrow(new DeletedException('Project', project.projectId)); + }); + it('Create project rule fails if project does not exist', async () => { + const car = await createUniqueCar(orgId); + const { leafRule1 } = await setupRules(car); + await expect(RulesService.createProjectRule(admin, organization, leafRule1.ruleId, 'fake-project-id')).rejects.toThrow( + new NotFoundException('Project', 'fake-project-id') + ); + }); + it('Create project rule fails if project rule assignment already exists', async () => { + const car = await createUniqueCar(orgId); + const { leafRule1 } = await setupRules(car); + await RulesService.createProjectRule(admin, organization, leafRule1.ruleId, project.projectId); + await expect(RulesService.createProjectRule(admin, organization, leafRule1.ruleId, project.projectId)).rejects.toThrow( + new HttpException(400, 'This rule is already associated with the project') + ); + }); + + // Updating Project Rule Status + it('Updates a project rule status successfully', async () => { + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + const projectRule = await RulesService.createProjectRule(admin, organization, topLevelRule.ruleId, project.projectId); + + const updatedProjectRule = await RulesService.editProjectRuleStatus( + admin, + organization, + projectRule.projectRuleId, + Rule_Completion.COMPLETED + ); + + expect(updatedProjectRule.projectRuleId).toBe(projectRule.projectRuleId); + expect(updatedProjectRule.currentStatus).toBe(Rule_Completion.COMPLETED); + expect(updatedProjectRule.statusHistory.length).toBe(1); + expect(updatedProjectRule.statusHistory[0].newStatus).toBe(Rule_Completion.COMPLETED); + expect(updatedProjectRule.statusHistory[0].projectRuleId).toBe(projectRule.projectRuleId); + expect(updatedProjectRule.statusHistory[0].createdBy.userId).toBe(admin.userId); + expect(new Date(updatedProjectRule.statusHistory[0].dateCreated).getTime()).toBeGreaterThan(Date.now() - 10000); + }); + + it('Updates a project rule status to the same status', async () => { + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + const projectRule = await RulesService.createProjectRule(admin, organization, topLevelRule.ruleId, project.projectId); + + const updatedProjectRule = await RulesService.editProjectRuleStatus( + admin, + organization, + projectRule.projectRuleId, + Rule_Completion.REVIEW + ); + + expect(updatedProjectRule.projectRuleId).toBe(projectRule.projectRuleId); + expect(updatedProjectRule.currentStatus).toBe(Rule_Completion.REVIEW); + expect(updatedProjectRule.statusHistory).toHaveLength(0); + }); + + it('Update project rule fails if user does not have permission', async () => { + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + const projectRule = await RulesService.createProjectRule(admin, organization, topLevelRule.ruleId, project.projectId); + + await expect( + async () => + await RulesService.editProjectRuleStatus( + nonLeadership, + organization, + projectRule.projectRuleId, + Rule_Completion.REVIEW + ) + ).rejects.toThrow(new AccessDeniedException('You do not have permissions to update a project rule status')); + }); + }); + + describe('Edit Rule', () => { + it('Fails if user is not an admin', async () => { + const car = await createUniqueCar(orgId); + const { leafRule1 } = await setupRules(car); + await expect( + async () => + await RulesService.editRule( + guest, + 'Some rule content', + leafRule1.ruleId, + leafRule1.ruleCode, + ['newfile'], + organization + ) + ).rejects.toThrow(new AccessDeniedAdminOnlyException('edit a rule')); + }); + + it('Fails if rule doesn`t exist', async () => { + const car = await createUniqueCar(orgId); + const { leafRule1 } = await setupRules(car); + await expect( + async () => + await RulesService.editRule( + await createTestUser(batmanAppAdmin, orgId), + 'Some more rule content', + '1', + leafRule1.ruleCode, + ['samefile'], + organization + ) + ).rejects.toThrow(new NotFoundException('Rule', 1)); + }); + + it('Succeeds and edits a rule', async () => { + const car = await createUniqueCar(orgId); + const { leafRule1 } = await setupRules(car); + const updatedRule = await RulesService.editRule( + admin, + 'BRAND NEW RULE CONTENT', + leafRule1.ruleId, + leafRule1.ruleCode, + leafRule1.imageFileIds, + organization + ); + + expect(updatedRule.ruleContent).toEqual('BRAND NEW RULE CONTENT'); + }); + }); + + describe('Delete Ruleset', () => { + it('Deletes a ruleset successfully and returns the correct information', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1 } = await setupRules(car); + + // deactivate before deleting + await prisma.ruleset.update({ + where: { rulesetId: ruleset1.rulesetId }, + data: { active: false } + }); + + const totalRules = await prisma.rule.count({ + where: { rulesetId: ruleset1.rulesetId } + }); + const rulesWithTeams = await prisma.rule.count({ + where: { + rulesetId: ruleset1.rulesetId, + teams: { some: {} } + } + }); + const expectedPercentage = totalRules > 0 ? (rulesWithTeams / totalRules) * 100 : 0; + const deleted = await RulesService.deleteRuleset(ruleset1.rulesetId, admin.userId, organization.organizationId); + + expect(deleted).toBeDefined(); + expect(deleted.rulesetId).toBe(ruleset1.rulesetId); + expect(deleted.assignedPercentage).toBeCloseTo(expectedPercentage, 2); + }); + it('Throws error when trying to delete an active ruleset', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1 } = await setupRules(car); + + // Ensure the ruleset is active + await prisma.ruleset.update({ + where: { rulesetId: ruleset1.rulesetId }, + data: { active: true } + }); + + await expect( + RulesService.deleteRuleset(ruleset1.rulesetId, admin.userId, organization.organizationId) + ).rejects.toThrow('Cannot delete an active ruleset. Please deactivate it first.'); + }); + it('Delete ruleset fails if user does not have permission', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1 } = await setupRules(car); + + await expect( + async () => await RulesService.deleteRuleset(ruleset1.rulesetId, nonLeadership.userId, organization.organizationId) + ).rejects.toThrow(new AccessDeniedException('Only admins can delete a ruleset.')); + }); + it('Delete ruleset fails if ruleset was already deleted', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1 } = await setupRules(car); + + // Deactivate the ruleset before deleting + await prisma.ruleset.update({ + where: { rulesetId: ruleset1.rulesetId }, + data: { active: false } + }); + + await RulesService.deleteRuleset(ruleset1.rulesetId, admin.userId, organization.organizationId); + await expect( + async () => await RulesService.deleteRuleset(ruleset1.rulesetId, admin.userId, organization.organizationId) + ).rejects.toThrow(new DeletedException('Ruleset', ruleset1.rulesetId)); + }); + it('Delete ruleset fails if ruleset does not exist', async () => { + await expect( + async () => await RulesService.deleteRuleset('fake-ruleset-id', admin.userId, organization.organizationId) + ).rejects.toThrow(new NotFoundException('Ruleset', 'fake-ruleset-id')); + }); + }); + + describe('Get all ruleset types', () => { + it('Successful get all ruleset types', async () => { + const rulesetTypes = await RulesService.getAllRulesetTypes(organization); + expect(rulesetTypes.length).toEqual(2); + expect(rulesetTypes[0].name).toEqual('FSAE'); + expect(rulesetTypes[1].name).toEqual('Ruleset Type with no Active Rulesets or Anything'); + }); + it('Get all ruleset types successful after adding ruleset type', async () => { + await prisma.ruleset_Type.create({ + data: { + name: 'FSAE2', + createdByUserId: admin.userId, + organizationId: orgId + } + }); + const rulesetTypes = await RulesService.getAllRulesetTypes(organization); + expect(rulesetTypes.length).toEqual(3); + expect(rulesetTypes[2].name).toEqual('FSAE2'); + }); + it('Get all ruleset types successful after deleting ruleset type', async () => { + await prisma.ruleset_Type.update({ + where: { + rulesetTypeId: fsaeRulesetType.rulesetTypeId + }, + data: { + deletedByUserId: admin.userId + } + }); + const rulesetTypes = await RulesService.getAllRulesetTypes(organization); + expect(rulesetTypes.length).toEqual(1); + }); + }); + + describe('Get Active Ruleset', () => { + it('Fails if user is a guest', async () => { + await expect(RulesService.getActiveRuleset(guest, fsaeRulesetType.rulesetTypeId, organization)).rejects.toThrow( + new AccessDeniedException('only members and above can view ruleset types!') + ); + }); + + it('Fails if ruleset type does not exist', async () => { + await expect(RulesService.getActiveRuleset(admin, 'fake-ruleset-type-id', organization)).rejects.toThrow( + new NotFoundException('Ruleset Type', 'fake-ruleset-type-id') + ); + }); + + it('Fails if ruleset type is already deleted', async () => { + await prisma.ruleset_Type.update({ + where: { + rulesetTypeId: emptyRulesetType.rulesetTypeId + }, + data: { + deletedByUserId: admin.userId + } + }); + + await expect(RulesService.getActiveRuleset(admin, emptyRulesetType.rulesetTypeId, organization)).rejects.toThrow( + new DeletedException('Ruleset Type', emptyRulesetType.rulesetTypeId) + ); + }); + + it('Fails if there are no rulesets in the given ruleset type', async () => { + await expect(RulesService.getActiveRuleset(admin, emptyRulesetType.rulesetTypeId, organization)).rejects.toThrow( + new NotFoundException('Active Ruleset for given Ruleset Type', emptyRulesetType.rulesetTypeId) + ); + }); + + it('Successfully gets the active ruleset for a ruleset type', async () => { + await setupRules(await createUniqueCar(orgId)); + + const activeRuleset = await RulesService.getActiveRuleset(admin, fsaeRulesetType.rulesetTypeId, organization); + expect(activeRuleset).toBeDefined(); + if (Array.isArray(activeRuleset)) { + throw new Error('Expected a single active ruleset, but got an array'); + } + + expect(activeRuleset.name).toBe('FSAE Rules 2025'); + expect(activeRuleset.active).toBe(true); + }); + }); + + describe('Toggle Rule Team', () => { + it('Fails if user is a guest', async () => { + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + await expect( + async () => await RulesService.toggleRuleTeam(topLevelRule.ruleId, '', guest, organization) + ).rejects.toThrow(new AccessDeniedGuestException('Toggle Rule Team')); + }); + it('Fails if rule does not exist', async () => { + await expect(async () => await RulesService.toggleRuleTeam('fake-rule-id', '', admin, organization)).rejects.toThrow( + new NotFoundException('Rule', 'fake-rule-id') + ); + }); + it('Fails if rule is deleted', async () => { + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + await RulesService.deleteRule(topLevelRule.ruleId, admin, organization); + await expect( + async () => await RulesService.toggleRuleTeam(topLevelRule.ruleId, '', admin, organization) + ).rejects.toThrow(new DeletedException('Rule', topLevelRule.ruleId)); + }); + it('Fails if a team does not exist', async () => { + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + await expect( + async () => await RulesService.toggleRuleTeam(topLevelRule.ruleId, 'fake-team-id', admin, organization) + ).rejects.toThrow(new NotFoundException('Team', 'fake-team-id')); + }); + it('Fails if a team is not in the correct organization', async () => { + const user = await prisma.user.create({ + data: { + firstName: 'Admin', + lastName: 'Admin', + email: 'testemail@hotmail.com', + googleAuthId: 'orgCreator1' + } + }); + const org2 = await prisma.organization.create({ + data: { + name: 'Joe mama', + description: 'Joe mama`s organization', + applicationLink: '', + userCreated: { + connect: { + userId: user.userId + } + } + } + }); + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + const teamType = await createTestTeamType('electrical', org2.organizationId); + const team = await createTestTeam(admin.userId, teamType.teamTypeId, org2.organizationId); + await expect(RulesService.toggleRuleTeam(topLevelRule.ruleId, team.teamId, admin, organization)).rejects.toThrow( + new InvalidOrganizationException('Rule') + ); + }); + it('Fails if a team is archived', async () => { + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + const teamType = await createTestTeamType('electrical', organization.organizationId); + const team = await createTestTeam(admin.userId, teamType.teamTypeId, organization.organizationId); + await TeamsService.archiveTeam(admin, team.teamId, organization); + await expect(RulesService.toggleRuleTeam(topLevelRule.ruleId, team.teamId, admin, organization)).rejects.toThrow( + new HttpException(400, 'Cannot toggle an archived team.') + ); + }); + it('Successfully adds a team to a rule', async () => { + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + const teamType = await createTestTeamType('electrical', organization.organizationId); + const team = await createTestTeam(admin.userId, teamType.teamTypeId, organization.organizationId); + const updRule = await RulesService.toggleRuleTeam(topLevelRule.ruleId, team.teamId, admin, organization); + const ruleWithTeams = await prisma.rule.findUnique({ + where: { ruleId: topLevelRule.ruleId }, + include: { teams: true } + }); + expect(updRule).toBeDefined(); + expect(ruleWithTeams?.teams.length).toBe(1); + expect(ruleWithTeams?.teams[0].teamId).toBe(team.teamId); + }); + it('Successfully removes a team from a rule', async () => { + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + const teamType = await createTestTeamType('electrical', organization.organizationId); + const team = await createTestTeam(admin.userId, teamType.teamTypeId, organization.organizationId); + const teamAddedRule = await RulesService.toggleRuleTeam(topLevelRule.ruleId, team.teamId, admin, organization); + expect(teamAddedRule).toBeDefined(); + + const teamRemovedRule = await RulesService.toggleRuleTeam(topLevelRule.ruleId, team.teamId, admin, organization); + const ruleWithTeams = await prisma.rule.findUnique({ + where: { ruleId: topLevelRule.ruleId }, + include: { teams: true } + }); + expect(teamRemovedRule).toBeDefined(); + expect(ruleWithTeams?.teams.length).toBe(0); + expect(ruleWithTeams?.teams[0]).toBeUndefined(); + }); + }); + + describe('Delete Project Rule', () => { + it('Deletes a project rule successfully and returns the correct information', async () => { + const car = await createUniqueCar(orgId); + const { leafRule1 } = await setupRules(car); + const projectRule = await RulesService.createProjectRule(admin, organization, leafRule1.ruleId, project.projectId); + + await RulesService.editProjectRuleStatus(admin, organization, projectRule.projectRuleId, Rule_Completion.COMPLETED); + await RulesService.editProjectRuleStatus(admin, organization, projectRule.projectRuleId, Rule_Completion.INCOMPLETE); + + const deletedProjectRule = await RulesService.deleteProjectRule(projectRule.projectRuleId, admin, organization); + + expect(deletedProjectRule).toBeDefined(); + expect(deletedProjectRule.projectRuleId).toBe(projectRule.projectRuleId); + + const statusChanges = await prisma.rule_Status_Change.findMany({ + where: { projectRuleId: projectRule.projectRuleId } + }); + expect(statusChanges.length).toBeGreaterThan(0); + statusChanges.forEach((statusChange) => { + expect(statusChange.dateDeleted).toBeDefined(); + // expect(statusChange.deletedByUserId).toBe(admin.userId); + }); + }); + it('Delete project rule fails if user does not have permission', async () => { + const car = await createUniqueCar(orgId); + const { leafRule1 } = await setupRules(car); + const projectRule = await RulesService.createProjectRule(admin, organization, leafRule1.ruleId, project.projectId); + + await expect( + async () => await RulesService.deleteProjectRule(projectRule.projectRuleId, nonLeadership, organization) + ).rejects.toThrow(new AccessDeniedAdminOnlyException('delete project rules')); + }); + it('Delete project rule fails if project rule was already deleted', async () => { + const car = await createUniqueCar(orgId); + const { leafRule1 } = await setupRules(car); + const projectRule = await RulesService.createProjectRule(admin, organization, leafRule1.ruleId, project.projectId); + + await RulesService.deleteProjectRule(projectRule.projectRuleId, admin, organization); + await expect( + async () => await RulesService.deleteProjectRule(projectRule.projectRuleId, admin, organization) + ).rejects.toThrow(new DeletedException('Project Rule', projectRule.projectRuleId)); + }); + it('Delete project rule fails if project rule does not exist', async () => { + await expect( + async () => await RulesService.deleteProjectRule('fake-project-rule-id', admin, organization) + ).rejects.toThrow(new NotFoundException('Project Rule', 'fake-project-rule-id')); + }); + }); + + describe('Delete Ruleset Type', () => { + it('Fails if user not an admin', async () => { + await expect(async () => await RulesService.deleteRulesetType(nonLeadership, 'FSAE', organization)).rejects.toThrow( + new AccessDeniedAdminOnlyException('delete ruleset types') + ); + }); + + it('Fails if the ruleset type has already been deleted', async () => { + const appAdmin = await createTestUser(batmanAppAdmin, orgId); + await RulesService.deleteRulesetType(appAdmin, fsaeRulesetType.rulesetTypeId, organization); + + await expect(RulesService.deleteRulesetType(appAdmin, fsaeRulesetType.rulesetTypeId, organization)).rejects.toThrow( + new DeletedException('Ruleset Type', fsaeRulesetType.rulesetTypeId) + ); + }); + + it('Successfully deletes the ruleset type', async () => { + let rulesetTypes = await RulesService.getAllRulesetTypes(organization); + expect(rulesetTypes.length).toEqual(2); + + const appAdmin = await createTestUser(batmanAppAdmin, orgId); + const result = await RulesService.deleteRulesetType(appAdmin, fsaeRulesetType.rulesetTypeId, organization); + + rulesetTypes = await RulesService.getAllRulesetTypes(organization); + + expect(rulesetTypes.length).toEqual(1); + + expect(result.rulesetTypeId).toBe(fsaeRulesetType.rulesetTypeId); + }); + + it('Successfully deletes all revision files in revision files', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1 } = await setupRules(car); + const revFiles: Ruleset[] = [ruleset1]; + + const fsaeRulesetType2WithRevisionFiles = await prisma.ruleset_Type.create({ + data: { + name: 'FSAE2', + createdBy: { connect: { userId: admin.userId } }, + organization: { connect: { organizationId: organization.organizationId } }, + revisionFiles: { connect: revFiles } + } + }); + + let rulesets = await RulesService.getRulesetsByRulesetType(fsaeRulesetType2WithRevisionFiles.rulesetTypeId, orgId); + expect(rulesets.length).toBe(1); + await RulesService.deleteRulesetType(admin, fsaeRulesetType2WithRevisionFiles.rulesetTypeId, organization); + rulesets = await RulesService.getRulesetsByRulesetType(fsaeRulesetType2WithRevisionFiles.rulesetTypeId, orgId); + expect(rulesets.length).toBe(0); + }); + }); + + describe('Get Unassigned Rules', () => { + it('Successfully gets unassigned rules', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1 } = await setupRules(car); + + const unassignedRules = await RulesService.getUnassignedRules(ruleset1.rulesetId, organization); + + expect(unassignedRules.length).toBe(3); + expect(unassignedRules.map((r) => r.ruleCode)).toContain('T'); + expect(unassignedRules.map((r) => r.ruleCode)).toContain('T2'); + expect(unassignedRules.map((r) => r.ruleCode)).toContain('T2.1'); + }); + + it('Returns only rules with no team assignments', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1, topLevelRule, leafRule1, leafRule2 } = await setupRules(car); + + const teamType = await createTestTeamType('TestTeamType', orgId); + const team = await createTestTeam(admin.userId, teamType.teamTypeId, orgId); + await prisma.rule.update({ + where: { ruleId: topLevelRule.ruleId }, + data: { + teams: { connect: { teamId: team.teamId } } + } + }); + + const unassignedRules = await RulesService.getUnassignedRules(ruleset1.rulesetId, organization); + + expect(unassignedRules.length).toBe(2); + expect(unassignedRules.map((r) => r.ruleId)).not.toContain(topLevelRule.ruleId); + expect(unassignedRules.map((r) => r.ruleId)).toContain(leafRule1.ruleId); + expect(unassignedRules.map((r) => r.ruleId)).toContain(leafRule2.ruleId); + }); + + it('Returns only non-deleted rules', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1, topLevelRule, leafRule1, leafRule2 } = await setupRules(car); + + // Delete one rule + await prisma.rule.update({ + where: { ruleId: leafRule1.ruleId }, + data: { dateDeleted: new Date(), deletedBy: { connect: { userId: admin.userId } } } + }); + + const unassignedRules = await RulesService.getUnassignedRules(ruleset1.rulesetId, organization); + + expect(unassignedRules.length).toBe(2); + expect(unassignedRules.map((r) => r.ruleId)).not.toContain(leafRule1.ruleId); + expect(unassignedRules.map((r) => r.ruleId)).toContain(topLevelRule.ruleId); + expect(unassignedRules.map((r) => r.ruleId)).toContain(leafRule2.ruleId); + }); + + it('Returns rules ordered by ruleCode ascending', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1 } = await setupRules(car); + + // Create additional rules with different codes + await prisma.rule.create({ + data: { + ruleCode: 'A.1', + ruleContent: 'Rule A', + imageFileIds: [], + ruleset: { connect: { rulesetId: ruleset1.rulesetId } }, + createdBy: { connect: { userId: admin.userId } } + } + }); + + await prisma.rule.create({ + data: { + ruleCode: 'Z.1', + ruleContent: 'Rule Z', + imageFileIds: [], + ruleset: { connect: { rulesetId: ruleset1.rulesetId } }, + createdBy: { connect: { userId: admin.userId } } + } + }); + + const unassignedRules = await RulesService.getUnassignedRules(ruleset1.rulesetId, organization); + + expect(unassignedRules.length).toBe(5); + // Check that rules are sorted by ruleCode + for (let i = 0; i < unassignedRules.length - 1; i++) { + expect(unassignedRules[i].ruleCode <= unassignedRules[i + 1].ruleCode).toBe(true); + } + }); + + it('Returns empty array when all rules are assigned to teams', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1, topLevelRule, leafRule1, leafRule2 } = await setupRules(car); + + // Create a team and assign all rules to it + const teamType = await createTestTeamType('TestTeamType', orgId); + const team = await createTestTeam(admin.userId, teamType.teamTypeId, orgId); + await prisma.rule.updateMany({ + where: { rulesetId: ruleset1.rulesetId }, + data: {} + }); + + await prisma.rule.update({ + where: { ruleId: topLevelRule.ruleId }, + data: { + teams: { connect: { teamId: team.teamId } } + } + }); + + await prisma.rule.update({ + where: { ruleId: leafRule1.ruleId }, + data: { + teams: { connect: { teamId: team.teamId } } + } + }); + + await prisma.rule.update({ + where: { ruleId: leafRule2.ruleId }, + data: { + teams: { connect: { teamId: team.teamId } } + } + }); + + const unassignedRules = await RulesService.getUnassignedRules(ruleset1.rulesetId, organization); + + expect(unassignedRules.length).toBe(0); + }); + + it('Returns empty array when all rules are deleted', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1, topLevelRule, leafRule1, leafRule2 } = await setupRules(car); + + // Delete all rules + await prisma.rule.update({ + where: { ruleId: topLevelRule.ruleId }, + data: { dateDeleted: new Date(), deletedBy: { connect: { userId: admin.userId } } } + }); + + await prisma.rule.update({ + where: { ruleId: leafRule1.ruleId }, + data: { dateDeleted: new Date(), deletedBy: { connect: { userId: admin.userId } } } + }); + + await prisma.rule.update({ + where: { ruleId: leafRule2.ruleId }, + data: { dateDeleted: new Date(), deletedBy: { connect: { userId: admin.userId } } } + }); + + const unassignedRules = await RulesService.getUnassignedRules(ruleset1.rulesetId, organization); + + expect(unassignedRules.length).toBe(0); + }); + + it('Fails when ruleset does not exist', async () => { + await expect(RulesService.getUnassignedRules('fake-ruleset-id', organization)).rejects.toThrow( + new NotFoundException('Ruleset', 'fake-ruleset-id') + ); + }); + + it('Fails when ruleset is deleted', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1 } = await setupRules(car); + + // Deactivate the ruleset before deleting + await prisma.ruleset.update({ + where: { rulesetId: ruleset1.rulesetId }, + data: { active: false } + }); + + await RulesService.deleteRuleset(ruleset1.rulesetId, admin.userId, organization.organizationId); + + await expect(RulesService.getUnassignedRules(ruleset1.rulesetId, organization)).rejects.toThrow( + new DeletedException('Ruleset', ruleset1.rulesetId) + ); + }); + + it('Returns rules with parent and subRules correctly transformed', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1, topLevelRule, leafRule1 } = await setupRules(car); + + const unassignedRules = await RulesService.getUnassignedRules(ruleset1.rulesetId, organization); + + const topRule = unassignedRules.find((r) => r.ruleId === topLevelRule.ruleId); + const leafRule = unassignedRules.find((r) => r.ruleId === leafRule1.ruleId); + + expect(topRule).toBeDefined(); + expect(topRule?.parentRule).toBeUndefined(); + expect(topRule?.subRuleIds).toContain(leafRule1.ruleId); + + expect(leafRule).toBeDefined(); + expect(leafRule?.parentRule?.ruleId).toBe(topLevelRule.ruleId); + expect(leafRule?.parentRule?.ruleCode).toBe(topLevelRule.ruleCode); + }); + }); + + describe('Get unassigned Rules - unassigned to project', () => { + it('fails if ruleset is in the wrong org', async () => { + const car = await createUniqueCar(orgId); + const otherOrgRulesetType = await prisma.ruleset_Type.create({ + data: { + name: 'Other Org FHE', + createdByUserId: admin.userId, + organizationId: otherOrg.organizationId + } + }); + const otherRuleset: Ruleset = await prisma.ruleset.create({ + data: { + name: '2024', + fileId: 'other-fhe-2024', + active: true, + rulesetTypeId: otherOrgRulesetType.rulesetTypeId, + carId: car.carId, + createdByUserId: admin.userId + } + }); + await expect( + RulesService.getUnassignedRulesForRuleset(otherRuleset.rulesetId, testTeam.teamId, organization.organizationId) + ).rejects.toThrow(InvalidOrganizationException); + }); + it('fails if team is in the wrong org', async () => { + const car = await createUniqueCar(orgId); + const otherTeam = await prisma.team.create({ + data: { + teamName: 'Other Team', + slackId: 'other-slack', + headId: admin.userId, + organizationId: otherOrg.organizationId + } + }); + const { ruleset1 } = await setupRules(car); + await expect( + RulesService.getUnassignedRulesForRuleset(ruleset1.rulesetId, otherTeam.teamId, organization.organizationId) + ).rejects.toThrow(InvalidOrganizationException); + }); + it('fails if ruleset does not exist', async () => { + await expect( + RulesService.getUnassignedRulesForRuleset('nonexistent-ruleset-id', testTeam.teamId, organization.organizationId) + ).rejects.toThrow(new NotFoundException('Ruleset', 'nonexistent-ruleset-id')); + }); + it('fails if team does not exist', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1 } = await setupRules(car); + await expect( + RulesService.getUnassignedRulesForRuleset(ruleset1.rulesetId, 'fake-team-id', organization.organizationId) + ).rejects.toThrow(new NotFoundException('Team', 'fake-team-id')); + }); + it('successfully returns rules in the team that have no projects', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1, topLevelRule, leafRule1, leafRule2 } = await setupRules(car); + // add rules to the team + await prisma.rule.update({ + where: { ruleId: topLevelRule.ruleId }, + data: { + teams: { + connect: { teamId: testTeam.teamId } + } + } + }); + await prisma.rule.update({ + where: { ruleId: leafRule1.ruleId }, + data: { + teams: { + connect: { teamId: testTeam.teamId } + } + } + }); + // rule in the team that has a project + const ruleWithProject = await prisma.rule.create({ + data: { + ruleCode: 'T.1.3', + ruleContent: 'Rule with project', + imageFileIds: [], + rulesetId: ruleset1.rulesetId, + createdByUserId: admin.userId, + teams: { + connect: { teamId: testTeam.teamId } + } + } + }); + await prisma.project_Rule.create({ + data: { + projectId: project.projectId, + ruleId: ruleWithProject.ruleId, + currentStatus: Rule_Completion.REVIEW, + createdByUserId: admin.userId + } + }); + const rules = await RulesService.getUnassignedRulesForRuleset( + ruleset1.rulesetId, + testTeam.teamId, + organization.organizationId + ); + expect(rules.length).toEqual(2); + expect(rules[0].ruleCode).toEqual('T'); + expect(rules[1].ruleCode).toEqual('T2'); + // leafRule2 is not in the team so should not be returned + expect(rules.find((r) => r.ruleCode === leafRule2.ruleCode)).toBeUndefined(); + // ruleWithProject has a project so should not be returned + expect(rules.find((r) => r.ruleCode === ruleWithProject.ruleCode)).toBeUndefined(); + }); + it('successfully returns empty if team has no assigned rules', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1 } = await setupRules(car); + const rules = await RulesService.getUnassignedRulesForRuleset( + ruleset1.rulesetId, + testTeam.teamId, + organization.organizationId + ); + expect(rules).toEqual([]); + }); + }); + + describe('Get Project Rules', () => { + it('Successfully gets all project rules for a project', async () => { + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + const projectRule = await RulesService.createProjectRule(admin, organization, topLevelRule.ruleId, project.projectId); + + const projectRules = await RulesService.getProjectRules(topLevelRule.rulesetId, projectRule.projectId, organization); + + expect(projectRules.length).toBe(1); + expect(projectRules[0].projectRuleId).toBe(projectRule.projectRuleId); + expect(projectRules[0].rule.ruleId).toBe(topLevelRule.ruleId); + }); + + it('Get project rules returns empty array if no project rules exist for the project', async () => { + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + + const projectRules = await RulesService.getProjectRules(topLevelRule.rulesetId, project.projectId, organization); + expect(projectRules.length).toBe(0); + }); + + it('Get project rules fails if project is deleted', async () => { + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + await prisma.project.update({ + where: { projectId: project.projectId }, + data: { + wbsElement: { + update: { dateDeleted: new Date() } + } + } + }); + + await expect( + async () => await RulesService.getProjectRules(topLevelRule.rulesetId, project.projectId, organization) + ).rejects.toThrow(new DeletedException('Project', project.projectId)); + }); + + it('Get project rules fails if ruleset does not exist', async () => { + await expect( + async () => await RulesService.getProjectRules('fake-ruleset-id', project.projectId, organization) + ).rejects.toThrow(new NotFoundException('Ruleset', 'fake-ruleset-id')); + }); + + it('Get project rules fails if project does not exist', async () => { + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + + await expect( + async () => await RulesService.getProjectRules(topLevelRule.rulesetId, 'fake-project-id', organization) + ).rejects.toThrow(new NotFoundException('Project', 'fake-project-id')); + }); + + it('Get project rules fails if ruleset is deleted', async () => { + const car = await createUniqueCar(orgId); + const { topLevelRule } = await setupRules(car); + await prisma.ruleset.update({ + where: { rulesetId: topLevelRule.rulesetId }, + data: { dateDeleted: new Date(), deletedBy: { connect: { userId: admin.userId } } } + }); + + await expect( + async () => await RulesService.getProjectRules(topLevelRule.rulesetId, project.projectId, organization) + ).rejects.toThrow(new DeletedException('Ruleset', topLevelRule.rulesetId)); + }); + }); + + describe('Get Team Rules in Ruleset Type', () => { + let team: Team; + let otherTeam: Team; + let activeRuleset: Ruleset; + let inactiveRuleset: Ruleset; + + beforeEach(async () => { + const car = await createUniqueCar(orgId); + + activeRuleset = await prisma.ruleset.create({ + data: { + name: 'FSAE Rules 2025', + fileId: 'fsae-rules-2025', + active: true, + car: { connect: { carId: car.carId } }, + createdBy: { connect: { userId: admin.userId } }, + rulesetType: { connect: { rulesetTypeId: fsaeRulesetType.rulesetTypeId } } + } + }); + + inactiveRuleset = await prisma.ruleset.create({ + data: { + name: 'FSAE Rules 2024', + fileId: 'fsae-rules-2024', + active: false, + car: { connect: { carId: car.carId } }, + createdBy: { connect: { userId: admin.userId } }, + rulesetType: { connect: { rulesetTypeId: fsaeRulesetType.rulesetTypeId } } + } + }); + + const teamType = await createTestTeamType(undefined, orgId); + team = await createTestTeam(admin.userId, teamType.teamTypeId, orgId); + const otherTeamHead = await createTestUser(batmanAppAdmin, orgId); + otherTeam = await createTestTeam(otherTeamHead.userId, teamType.teamTypeId, orgId); + }); + + it('Succeeds and returns rules assigned to team in active ruleset', async () => { + const rule1 = await RulesService.createRule(admin, 'T.1.1', 'Rule 1', activeRuleset.rulesetId, organization); + const rule2 = await RulesService.createRule(admin, 'T.1.2', 'Rule 2', activeRuleset.rulesetId, organization); + const rule3 = await RulesService.createRule(admin, 'T.1.3', 'Rule 3', activeRuleset.rulesetId, organization); + + await prisma.rule.update({ + where: { ruleId: rule1.ruleId }, + data: { teams: { connect: { teamId: team.teamId } } } + }); + await prisma.rule.update({ + where: { ruleId: rule2.ruleId }, + data: { teams: { connect: { teamId: team.teamId } } } + }); + + const rules = await RulesService.getTeamRulesInRulesetType(team.teamId, fsaeRulesetType.rulesetTypeId, organization); + + expect(rules.length).toBe(2); + expect(rules.map((r) => r.ruleId)).toContain(rule1.ruleId); + expect(rules.map((r) => r.ruleId)).toContain(rule2.ruleId); + expect(rules.map((r) => r.ruleId)).not.toContain(rule3.ruleId); + }); + + it('Fails when no active ruleset exists', async () => { + await prisma.ruleset.update({ + where: { rulesetId: activeRuleset.rulesetId }, + data: { active: false } + }); + + await expect( + RulesService.getTeamRulesInRulesetType(team.teamId, fsaeRulesetType.rulesetTypeId, organization) + ).rejects.toThrow(new NotFoundException('Active Ruleset for given Ruleset Type', activeRuleset.rulesetTypeId)); + }); + + it('Only returns rules from active ruleset, not inactive', async () => { + const activeRule = await RulesService.createRule(admin, 'T.1.1', 'Active Rule', activeRuleset.rulesetId, organization); + const inactiveRule = await RulesService.createRule( + admin, + 'T.2.1', + 'Inactive Rule', + inactiveRuleset.rulesetId, + organization + ); + + await prisma.rule.update({ + where: { ruleId: activeRule.ruleId }, + data: { teams: { connect: { teamId: team.teamId } } } + }); + await prisma.rule.update({ + where: { ruleId: inactiveRule.ruleId }, + data: { teams: { connect: { teamId: team.teamId } } } + }); + + const rules = await RulesService.getTeamRulesInRulesetType(team.teamId, fsaeRulesetType.rulesetTypeId, organization); + + expect(rules.length).toBe(1); + expect(rules[0].ruleId).toBe(activeRule.ruleId); + }); + + it('Does not return deleted rules', async () => { + const rule1 = await RulesService.createRule(admin, 'T.1.1', 'Rule 1', activeRuleset.rulesetId, organization); + const rule2 = await RulesService.createRule(admin, 'T.1.2', 'Rule 2', activeRuleset.rulesetId, organization); + + await prisma.rule.update({ + where: { ruleId: rule1.ruleId }, + data: { teams: { connect: { teamId: team.teamId } } } + }); + await prisma.rule.update({ + where: { ruleId: rule2.ruleId }, + data: { teams: { connect: { teamId: team.teamId } } } + }); + + await RulesService.deleteRule(rule1.ruleId, admin, organization); + + const rules = await RulesService.getTeamRulesInRulesetType(team.teamId, fsaeRulesetType.rulesetTypeId, organization); + + expect(rules.length).toBe(1); + expect(rules[0].ruleId).toBe(rule2.ruleId); + }); + + it('Only returns rules assigned to the specified team', async () => { + const rule1 = await RulesService.createRule(admin, 'T.1.1', 'Rule 1', activeRuleset.rulesetId, organization); + const rule2 = await RulesService.createRule(admin, 'T.1.2', 'Rule 2', activeRuleset.rulesetId, organization); + + await prisma.rule.update({ + where: { ruleId: rule1.ruleId }, + data: { teams: { connect: { teamId: team.teamId } } } + }); + await prisma.rule.update({ + where: { ruleId: rule2.ruleId }, + data: { teams: { connect: { teamId: otherTeam.teamId } } } + }); + + const rules = await RulesService.getTeamRulesInRulesetType(team.teamId, fsaeRulesetType.rulesetTypeId, organization); + + expect(rules.length).toBe(1); + expect(rules[0].ruleId).toBe(rule1.ruleId); + }); + + it('Fails if ruleset type does not exist', async () => { + await expect( + RulesService.getTeamRulesInRulesetType(team.teamId, 'fake-ruleset-type-id', organization) + ).rejects.toThrow(new NotFoundException('Ruleset Type', 'fake-ruleset-type-id')); + }); + + it('Fails if ruleset type is deleted', async () => { + await RulesService.deleteRulesetType(admin, fsaeRulesetType.rulesetTypeId, organization); + + await expect( + RulesService.getTeamRulesInRulesetType(team.teamId, fsaeRulesetType.rulesetTypeId, organization) + ).rejects.toThrow(new DeletedException('Ruleset Type', fsaeRulesetType.rulesetTypeId)); + }); + + it('Fails if team does not exist', async () => { + await expect( + RulesService.getTeamRulesInRulesetType('fake-team-id', fsaeRulesetType.rulesetTypeId, organization) + ).rejects.toThrow(new NotFoundException('Team', 'fake-team-id')); + }); + }); + + describe('Get Top Level Rules', () => { + it('Successful get all rules with no parent id', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1, topLevelRule } = await setupRules(car); + + const rules = await RulesService.getTopLevelRules(ruleset1.rulesetId, organization.organizationId); + + expect(rules.length).toEqual(1); + expect(rules[0].ruleCode).toEqual('T'); + expect(rules[0].ruleId).toEqual(topLevelRule.ruleId); + }); + + it('Gets multiple top level rules', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1 } = await setupRules(car); + await prisma.rule.create({ + data: { + ruleCode: 'A', + ruleContent: 'PART A - ADMINISTRATIVE REQUIREMENTS', + imageFileIds: [], + dateCreated: new Date(), + ruleset: { connect: { rulesetId: ruleset1.rulesetId } }, + createdBy: { connect: { userId: admin.userId } } + } + }); + + const rules = await RulesService.getTopLevelRules(ruleset1.rulesetId, organization.organizationId); + + expect(rules.length).toEqual(2); + expect(rules.map((r) => r.ruleCode).sort()).toEqual(['A', 'T']); + }); + + it('Returns empty array when no top level rules exist', async () => { + const car = await createUniqueCar(orgId); + const ruleset = await prisma.ruleset.create({ + data: { + name: 'Empty Ruleset', + fileId: 'empty-ruleset', + active: true, + dateCreated: new Date(), + car: { connect: { carId: car.carId } }, + createdBy: { connect: { userId: admin.userId } }, + rulesetType: { connect: { rulesetTypeId: fsaeRulesetType.rulesetTypeId } } + } + }); + + const rules = await RulesService.getTopLevelRules(ruleset.rulesetId, organization.organizationId); + expect(rules.length).toEqual(0); + }); + + it('Does not return child rules', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1, topLevelRule, leafRule1, leafRule2 } = await setupRules(car); + const rules = await RulesService.getTopLevelRules(ruleset1.rulesetId, organization.organizationId); + + expect(rules.length).toEqual(1); + expect(rules[0].ruleId).toEqual(topLevelRule.ruleId); + expect(rules.find((r) => r.ruleId === leafRule1.ruleId)).toBeUndefined(); + expect(rules.find((r) => r.ruleId === leafRule2.ruleId)).toBeUndefined(); + }); + + it('Does not return deleted top level rules', async () => { + const carr = await createUniqueCar(orgId); + const { ruleset1, topLevelRule } = await setupRules(carr); + + await prisma.rule.update({ + where: { ruleId: topLevelRule.ruleId }, + data: { + dateDeleted: new Date(), + deletedByUserId: admin.userId + } + }); + + const rules = await RulesService.getTopLevelRules(ruleset1.rulesetId, organization.organizationId); + expect(rules.length).toEqual(0); + }); + }); + + describe('Get Ruleset Type', () => { + it('Successfully gets a ruleset type by ID', async () => { + const rulesetType = await RulesService.getRulesetType(fsaeRulesetType.rulesetTypeId, organization.organizationId); + expect(rulesetType).toBeDefined(); + expect(rulesetType.rulesetTypeId).toBe(fsaeRulesetType.rulesetTypeId); + expect(rulesetType.name).toBe(fsaeRulesetType.name); + }); + }); +}); diff --git a/src/frontend/src/apis/rules.api.ts b/src/frontend/src/apis/rules.api.ts new file mode 100644 index 0000000000..071e0bc891 --- /dev/null +++ b/src/frontend/src/apis/rules.api.ts @@ -0,0 +1,223 @@ +/* + * This file is part of NER's FinishLine and licensed under GNU AGPLv3. + * See the LICENSE file in the repository root folder for details. + */ + +import axios from '../utils/axios'; +import { ProjectRule, Rule as SharedRule, RuleCompletion, RulesetType, Ruleset } from 'shared'; +import { apiUrls } from '../utils/urls'; +import { CreateRulesetPayload, ParseRulesetPayload, CreateRulePayload } from '../hooks/rules.hooks'; +import { + projectRuleTransformer, + rulesetTransformer, + rulesetTypeTransformer, + ruleTransformer +} from './transformers/rules.transformers'; + +/** + * Gets a ruleset by its ID + */ +export const getRulesetById = (rulesetId: string) => { + return axios.get(apiUrls.rulesetById(rulesetId), { + transformResponse: (data) => JSON.parse(data) + }); +}; + +/** + * Gets a single ruleset by ID (dashboard usage) + */ +export const getSingleRuleset = (rulesetId: string) => { + return axios.get(apiUrls.singleRuleset(rulesetId), { + transformResponse: (data) => rulesetTransformer(JSON.parse(data)) + }); +}; + +/** + * Toggles team assignment for a rule + */ +export const toggleRuleTeam = (ruleId: string, teamId: string) => { + return axios.post(apiUrls.rulesToggleTeam(ruleId), { teamId }); +}; + +/** + * Gets all rules assigned to a team for a specific ruleset type + */ +export const getTeamRulesInRulesetType = (rulesetTypeId: string, teamId: string) => { + return axios.get(apiUrls.rulesTeamRulesInRulesetType(rulesetTypeId, teamId)); +}; + +/** + * Creates a new ruleset type + */ +export const createRulesetType = (payload: { name: string }) => { + return axios.post(apiUrls.rulesetTypeCreate(), payload); +}; + +/** + * Creates a new rule + * + * @param payload the data for creating the rule + * @returns the created rule + */ +export const createRule = (payload: CreateRulePayload) => { + return axios.post(apiUrls.ruleCreate(), { ...payload }); +}; + +/** + * Fetches all Ruleset Types for the current organization. + * + * @returns A list of Ruleset Types. + */ +export const getAllRulesetTypes = () => { + return axios.get(apiUrls.rulesetTypes(), { + transformResponse: (data) => JSON.parse(data).map(rulesetTypeTransformer) + }); +}; + +/** + * Gets the active ruleset for a given ruleset type. + */ +export const getActiveRuleset = (rulesetTypeId: string) => { + return axios.get(apiUrls.rulesGetActiveRuleset(rulesetTypeId), { + transformResponse: (data) => rulesetTransformer(JSON.parse(data)) + }); +}; + +/** + * Gets all project rules for a given ruleset and project. + */ +export const getProjectRules = (rulesetId: string, projectId: string) => { + return axios.get(apiUrls.rulesGetProjectRules(rulesetId, projectId), { + transformResponse: (data) => JSON.parse(data).map(projectRuleTransformer) + }); +}; + +/** + * Gets unassigned rules for a ruleset and team. + */ +export const getUnassignedRulesForRuleset = (rulesetId: string, teamId: string) => { + return axios.get(apiUrls.rulesGetUnassignedRulesForRuleset(rulesetId, teamId), { + transformResponse: (data) => JSON.parse(data).map(ruleTransformer) + }); +}; + +/** + * Creates a project rule + */ +export const createProjectRule = (ruleId: string, projectId: string) => { + return axios.post(apiUrls.rulesCreateProjectRule(), { ruleId, projectId }); +}; + +/** + * Deletes a project rule + */ +export const deleteProjectRule = (projectRuleId: string) => { + return axios.post(apiUrls.rulesDeleteProjectRule(projectRuleId)); +}; + +/** + * Updates project rule status + */ +export const editProjectRuleStatus = (projectRuleId: string, newStatus: RuleCompletion) => { + return axios.post(apiUrls.rulesEditProjectRuleStatus(projectRuleId), { newStatus }); +}; + +/** + * Gets child rules + */ +export const getChildRules = (ruleId: string) => { + return axios.get(apiUrls.rulesChildRules(ruleId), { + transformResponse: (data) => JSON.parse(data).map(ruleTransformer) + }); +}; + +/** + * Gets top-level rules + */ +export const getTopLevelRules = (rulesetId: string) => { + return axios.get(apiUrls.rulesTopLevel(rulesetId), { + transformResponse: (data) => JSON.parse(data).map(ruleTransformer) + }); +}; + +/** + * Fetch rulesets by type + */ +export const getRulesetsByRulesetType = (rulesetTypeId: string) => { + return axios.get(apiUrls.rulesetsByType(rulesetTypeId), { + transformResponse: (data) => JSON.parse(data) + }); +}; + +/** + * Deletes a rule + */ +export const deleteRule = (ruleId: string) => { + return axios.post(apiUrls.rulesDelete(ruleId)); +}; + +/** + * Edits a rule's content + * @param ruleId - The ID of the rule to edit + * @param ruleContent - The new content for the rule + */ +export const editRule = (ruleId: string, ruleContent: string) => { + return axios.post(apiUrls.rulesEdit(ruleId), { ruleContent }); +}; + +/** + * Updates a rulesets active status + */ +export const updateRuleset = (rulesetId: string, name: string, isActive: boolean) => { + return axios.post(apiUrls.rulesetUpdate(rulesetId), { name, isActive }); +}; + +/** + * Deletes a ruleset + */ +export const deleteRuleset = (rulesetId: string) => { + return axios.post(apiUrls.rulesetDelete(rulesetId)); +}; + +/** + * Deletes a ruleset type + */ +export const deleteRulesetType = (rulesetTypeId: string) => { + return axios.post(apiUrls.rulesetTypeDelete(rulesetTypeId)); +}; + +/** + * Gets a ruleset type given its ID + */ +export const getRulesetType = (rulesetTypeId: string) => { + return axios.get(apiUrls.rulesetType(rulesetTypeId)); +}; + +/** + * Creates a new ruleset + */ +export const createRuleset = (payload: CreateRulesetPayload) => { + return axios.post(apiUrls.rulesetsCreate(), payload); +}; + +/** + * Parses a ruleset PDF + */ +export const parseRuleset = (payload: ParseRulesetPayload) => { + return axios.post(apiUrls.parseRuleset(payload.rulesetId), { + fileId: payload.fileId, + parserType: payload.parserType + }); +}; + +/** + * Upload ruleset PDF file + */ +export const uploadRulesetFile = (file: File) => { + const formData = new FormData(); + formData.append('file', file); + + return axios.post(apiUrls.uploadRulesetFile(), formData, { + transformResponse: (data) => JSON.parse(data) + }); +}; diff --git a/src/frontend/src/apis/transformers/rules.transformers.ts b/src/frontend/src/apis/transformers/rules.transformers.ts new file mode 100644 index 0000000000..7d187ed7b4 --- /dev/null +++ b/src/frontend/src/apis/transformers/rules.transformers.ts @@ -0,0 +1,65 @@ +/* + * This file is part of NER's FinishLine and licensed under GNU AGPLv3. + * See the LICENSE file in the repository root folder for details. + */ + +import { ProjectRule, Rule, RulesetType, Ruleset } from 'shared'; + +/** + * Transforms a rule to proper field types. + * + * @param rule Incoming rule object + * @returns Properly transformed rule object. + */ +export const ruleTransformer = (rule: Rule): Rule => { + return { + ...rule, + subRuleIds: rule.subRuleIds || [], + referencedRuleIds: rule.referencedRuleIds || [] + }; +}; + +/** + * Transforms a project rule (support Date objects) + * + * @param projectRule Incoming project rule object + * @returns Properly transformed project rule object. + */ +export const projectRuleTransformer = (projectRule: ProjectRule): ProjectRule => { + return { + ...projectRule, + rule: ruleTransformer(projectRule.rule), + statusHistory: (projectRule.statusHistory || []).map((history) => ({ + ...history, + dateCreated: new Date(history.dateCreated) + })) + }; +}; + +/** + * Transforms a ruleset type (support Date objects) + * + * @param rulesetType Incoming ruleset type object + * @returns Properly transformed ruleset type object. + */ +export const rulesetTypeTransformer = (rulesetType: RulesetType): RulesetType => { + return { + ...rulesetType, + lastUpdated: new Date(rulesetType.lastUpdated), + revisionFiles: rulesetType.revisionFiles || [] + }; +}; + +/** + * Transforms a ruleset (support Date objects) + * + * @param ruleset Incoming ruleset object + * @returns Properly transformed ruleset object. + */ +export const rulesetTransformer = (ruleset: Ruleset): Ruleset => { + return { + ...ruleset, + dateCreated: new Date(ruleset.dateCreated), + rulesetType: rulesetTypeTransformer(ruleset.rulesetType) + }; +}; diff --git a/src/frontend/src/app/AppAuthenticated.tsx b/src/frontend/src/app/AppAuthenticated.tsx index 9b58381a39..dee893b172 100644 --- a/src/frontend/src/app/AppAuthenticated.tsx +++ b/src/frontend/src/app/AppAuthenticated.tsx @@ -11,6 +11,7 @@ import { PageNotFound } from '../pages/PageNotFound'; import Home from '../pages/HomePage/Home'; import Settings from '../pages/SettingsPage/SettingsPage'; import InfoPage from '../pages/InfoPage'; +import Rules from '../pages/RulesPage/Rules'; import GanttChartPage from '../pages/GanttPage/ProjectGanttChart/ProjectGanttChartPage'; import Teams from '../pages/TeamsPage/Teams'; import AdminTools from '../pages/AdminToolsPage/AdminTools'; @@ -130,6 +131,7 @@ const AppAuthenticated: React.FC = ({ userId, userRole }) + diff --git a/src/frontend/src/hooks/rules.hooks.ts b/src/frontend/src/hooks/rules.hooks.ts new file mode 100644 index 0000000000..08113c250f --- /dev/null +++ b/src/frontend/src/hooks/rules.hooks.ts @@ -0,0 +1,556 @@ +/* + * This file is part of NER's FinishLine and licensed under GNU AGPLv3. + * See the LICENSE file in the repository root folder for details. + */ + +import { useMutation, useQuery, useQueryClient } from 'react-query'; +import { ProjectRule, Rule as SharedRule, RuleCompletion, Ruleset, RulesetType } from 'shared'; +import { + createRulesetType, + getAllRulesetTypes, + getActiveRuleset, + getProjectRules, + getUnassignedRulesForRuleset, + createProjectRule, + deleteProjectRule, + editProjectRuleStatus, + getChildRules, + getTopLevelRules, + toggleRuleTeam, + getTeamRulesInRulesetType, + parseRuleset, + uploadRulesetFile, + getRulesetsByRulesetType, + deleteRule, + editRule, + updateRuleset, + deleteRuleset, + deleteRulesetType, + createRuleset, + getRulesetById, + createRule, + getSingleRuleset, + getRulesetType +} from '../apis/rules.api'; +import { useToast } from './toasts.hooks'; + +/** + * Hook to supply all ruleset types. + */ +export const useAllRulesetTypes = () => { + return useQuery(['rules', 'rulesetTypes'], async () => { + const { data } = await getAllRulesetTypes(); + return data; + }); +}; + +/** + * Hook to get the active ruleset for a given ruleset type. + */ +export const useActiveRuleset = (rulesetTypeId: string) => { + return useQuery( + ['rules', 'activeRuleset', rulesetTypeId], + async () => { + try { + const { data } = await getActiveRuleset(rulesetTypeId); + return data; + } catch { + // Return undefined if no active ruleset exists + return undefined; + } + }, + { enabled: !!rulesetTypeId } + ); +}; + +/** + * Hook to get all project rules for a given ruleset and project. + */ +export const useProjectRules = (rulesetId: string, projectId: string) => { + return useQuery( + ['rules', 'projectRules', rulesetId, projectId], + async () => { + const { data } = await getProjectRules(rulesetId, projectId); + return data; + }, + { enabled: !!rulesetId && !!projectId } + ); +}; + +/** + * Hook to get unassigned rules for a ruleset and team. + */ +export const useUnassignedRulesForRuleset = (rulesetId: string, teamId: string) => { + return useQuery( + ['rules', 'unassigned', rulesetId, teamId], + async () => { + const { data } = await getUnassignedRulesForRuleset(rulesetId, teamId); + return data; + }, + { enabled: !!rulesetId && !!teamId } + ); +}; + +/** + * Hook to get child rules of a rule. + */ +export const useChildRules = (ruleId: string) => { + return useQuery( + ['rules', 'children', ruleId], + async () => { + const { data } = await getChildRules(ruleId); + return data; + }, + { enabled: !!ruleId } + ); +}; + +/** + * Hook to get top-level rules for a ruleset. + */ +export const useTopLevelRules = (rulesetId: string) => { + return useQuery( + ['rules', 'topLevel', rulesetId], + async () => { + const { data } = await getTopLevelRules(rulesetId); + return data; + }, + { enabled: !!rulesetId } + ); +}; + +interface CreateRulesetTypePayload { + name: string; +} + +export interface ParseRulesetPayload { + rulesetId: string; + fileId: string; + parserType: 'FSAE' | 'FHE'; +} + +export interface CreateRulesetPayload { + fileId: string; + name: string; + rulesetTypeId: string; + carNumber: number; + active: boolean; +} + +export interface CreateRulePayload { + ruleCode: string; + ruleContent: string; + rulesetId: string; + parentRuleId?: string; + referencedRules?: string[]; + imageFileIds?: string[]; +} + +export const useGetTopLevelRules = (rulesetId: string) => { + return useQuery(['rules', 'top-level', rulesetId], async () => { + const { data } = await getTopLevelRules(rulesetId); + return data; + }); +}; + +export const useGetChildRules = (ruleId: string, enabled: boolean = true) => { + return useQuery( + ['rules', 'children', ruleId], + async () => { + const { data } = await getChildRules(ruleId); + return data; + }, + { + enabled // only fetch when true + } + ); +}; + +/** + * Hook to get a ruleset by ID. + * (Kept because some parts of the app may still call getRulesetById) + */ +export const useGetRuleset = (rulesetId: string) => { + return useQuery( + ['ruleset', rulesetId], + async () => { + const { data } = await getRulesetById(rulesetId); + return data; + }, + { enabled: !!rulesetId } + ); +}; + +/** + * Hook to get a single ruleset by ID (kept for compatibility with feature branch usage) + */ +export const useSingleRuleset = (rulesetId: string) => { + return useQuery( + ['rules', 'ruleset', rulesetId], + async () => { + const { data } = await getSingleRuleset(rulesetId); + return data; + }, + { enabled: !!rulesetId } + ); +}; + +export const useToggleRuleTeam = () => { + const queryClient = useQueryClient(); + return useMutation( + ['rules', 'toggle-team'], + async ({ ruleId, teamId }) => { + const { data } = await toggleRuleTeam(ruleId, teamId); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['rules']); + } + } + ); +}; + +/** + * Hook to toggle multiple rule-team assignments in bulk + * Processes each toggle sequentially and returns aggregate results + */ +export const useBulkToggleRuleTeam = () => { + const queryClient = useQueryClient(); + const toast = useToast(); + + return useMutation< + { successful: number; failed: number; errors: string[] }, + Error, + Array<{ ruleId: string; teamId: string }> + >( + ['rules', 'bulk-toggle-team'], + async (toggles) => { + let successful = 0; + let failed = 0; + const errors: string[] = []; + + for (const { ruleId, teamId } of toggles) { + try { + await toggleRuleTeam(ruleId, teamId); + successful++; + } catch (error) { + failed++; + errors.push(`Failed to toggle rule ${ruleId}: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + + return { successful, failed, errors }; + }, + { + onSuccess: (result) => { + queryClient.invalidateQueries(['rules']); + + if (result.failed > 0) { + toast.error(`${result.failed} assignment(s) failed to save. ${result.successful} succeeded.`); + } else if (result.successful > 0) { + toast.success(`Successfully saved ${result.successful} assignment change(s)`); + } + }, + onError: (error: Error) => { + toast.error(`Failed to save assignments: ${error.message}`); + } + } + ); +}; + +export const useGetTeamRulesInRulesetType = (rulesetTypeId: string, teamId: string) => { + return useQuery(['rules', 'team-rules', rulesetTypeId, teamId], async () => { + const { data } = await getTeamRulesInRulesetType(rulesetTypeId, teamId); + return data; + }); +}; + +/** + * Hook to create a new ruleset type. + */ +export const useCreateRulesetType = () => { + const queryClient = useQueryClient(); + return useMutation( + ['rulesetTypes', 'create'], + async (payload: CreateRulesetTypePayload) => { + const { data } = await createRulesetType(payload); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['rules', 'rulesetTypes']); + } + } + ); +}; + +/** + * Custom React Hook to create a new rule + */ +export const useCreateRule = () => { + const queryClient = useQueryClient(); + return useMutation( + ['rules', 'create'], + async (payload: CreateRulePayload) => { + const { data } = await createRule(payload); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['rules']); + queryClient.invalidateQueries(['ruleset']); + } + } + ); +}; + +/** + * Hook to create a project rule (assign a rule to a project). + */ +export const useCreateProjectRule = () => { + const queryClient = useQueryClient(); + return useMutation( + ['rules', 'projectRules', 'create'], + async ({ ruleId, projectId: pId }) => { + const { data } = await createProjectRule(ruleId, pId); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['rules', 'projectRules']); + queryClient.invalidateQueries(['rules', 'unassigned']); + } + } + ); +}; + +/** + * Hook to delete a project rule. + */ +export const useDeleteProjectRule = (rulesetId: string, projectId: string) => { + const queryClient = useQueryClient(); + return useMutation( + ['rules', 'projectRules', 'delete'], + async (projectRuleId: string) => { + const { data } = await deleteProjectRule(projectRuleId); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['rules', 'projectRules', rulesetId, projectId]); + queryClient.invalidateQueries(['rules', 'unassigned']); + } + } + ); +}; + +/** + * Hook to update project rule status. + */ +export const useEditProjectRuleStatus = (rulesetId: string, projectId: string) => { + const queryClient = useQueryClient(); + return useMutation( + ['rules', 'projectRules', 'editStatus'], + async ({ projectRuleId, newStatus }) => { + const { data } = await editProjectRuleStatus(projectRuleId, newStatus); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['rules', 'projectRules', rulesetId, projectId]); + } + } + ); +}; + +/** + * React Query hook to fetch all Rulesets for a specific Ruleset Type. + * + * @param rulesetTypeId The ID of the ruleset type. + * @returns Query result containing Rulesets data, loading state, and error state. + */ +export const useRulesetsByType = (rulesetTypeId: string) => { + return useQuery(['rulesets', rulesetTypeId], async () => { + const { data } = await getRulesetsByRulesetType(rulesetTypeId); + return data; + }); +}; + +/** + * React Query hook to delete a rule + */ +export const useDeleteRule = () => { + const queryClient = useQueryClient(); + const toast = useToast(); + + return useMutation( + ['rules', 'delete'], + async (ruleId: string) => { + await deleteRule(ruleId); + }, + { + onSuccess: () => { + toast.success('Rule deleted successfully'); + queryClient.invalidateQueries(['rules']); + queryClient.invalidateQueries(['rulesets']); + }, + onError: (error: Error) => { + toast.error(error.message); + } + } + ); +}; + +/** + * React Query hook to edit a rule's content + */ +export const useEditRule = () => { + const queryClient = useQueryClient(); + const toast = useToast(); + + return useMutation( + ['rules', 'edit'], + async ({ ruleId, ruleContent }) => { + const { data } = await editRule(ruleId, ruleContent); + return data; + }, + { + onSuccess: () => { + toast.success('Rule updated successfully'); + queryClient.invalidateQueries(['rules']); + queryClient.invalidateQueries(['rulesets']); + }, + onError: (error: Error) => { + toast.error(`Failed to update rule: ${error.message}`); + } + } + ); +}; + +export const useUpdateRuleset = () => { + const queryClient = useQueryClient(); + return useMutation( + async ({ rulesetId, name, isActive }) => { + const { data } = await updateRuleset(rulesetId, name, isActive); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['rulesets']); + } + } + ); +}; + +export const useDeleteRuleset = () => { + const queryClient = useQueryClient(); + return useMutation( + async (rulesetId: string) => { + await deleteRuleset(rulesetId); + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['rulesets']); + } + } + ); +}; + +export const useDeleteRulesetType = () => { + const queryClient = useQueryClient(); + return useMutation( + async (rulesetTypeId: string) => { + await deleteRulesetType(rulesetTypeId); + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['rulesetTypes']); + } + } + ); +}; + +export const useRulesetType = (rulesetTypeId: string) => { + return useQuery(['rulesetType', rulesetTypeId], async () => { + const { data } = await getRulesetType(rulesetTypeId); + return data; + }); +}; +export const useCreateRuleset = () => { + const queryClient = useQueryClient(); + return useMutation( + ['rulesets', 'create'], + async (payload: CreateRulesetPayload) => { + const { data } = await createRuleset(payload); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['rulesets']); + } + } + ); +}; + +export const useParseRuleset = () => { + const queryClient = useQueryClient(); + return useMutation( + ['rulesets', 'parse'], + async (payload: ParseRulesetPayload) => { + const { data } = await parseRuleset(payload); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['rules']); + queryClient.invalidateQueries(['rulesets']); + } + } + ); +}; + +/** + * Uploads a file to the drive and returns the fileId + */ +export const useUploadRulesetFile = () => { + return useMutation(['ruleset-file', 'upload'], async (file: File) => { + const { data } = await uploadRulesetFile(file); + return data; + }); +}; + +/** + * Helper function to recursively fetch all child rules + */ +const fetchAllChildRules = async (rule: SharedRule, allRules: SharedRule[]): Promise => { + if (rule.subRuleIds.length === 0) return; + + const { data: children } = await getChildRules(rule.ruleId); + allRules.push(...children); + + for (const child of children) { + await fetchAllChildRules(child, allRules); + } +}; + +/** + * Hook to get all rules for a ruleset by fetching top-level rules + * and recursively fetching all children + */ +export const useAllRulesForRuleset = (rulesetId: string) => { + return useQuery( + ['rules', 'allRules', rulesetId], + async () => { + const { data: topLevelRules } = await getTopLevelRules(rulesetId); + const allRules: SharedRule[] = [...topLevelRules]; + + for (const rule of topLevelRules) { + await fetchAllChildRules(rule, allRules); + } + + return allRules; + }, + { enabled: !!rulesetId } + ); +}; diff --git a/src/frontend/src/layouts/Sidebar/Sidebar.tsx b/src/frontend/src/layouts/Sidebar/Sidebar.tsx index 6fd7b11f4d..5a988cc429 100644 --- a/src/frontend/src/layouts/Sidebar/Sidebar.tsx +++ b/src/frontend/src/layouts/Sidebar/Sidebar.tsx @@ -15,6 +15,7 @@ import GroupIcon from '@mui/icons-material/Group'; import QuestionMarkIcon from '@mui/icons-material/QuestionMark'; import AttachMoneyIcon from '@mui/icons-material/AttachMoney'; import CalendarTodayIcon from '@mui/icons-material/CalendarToday'; +import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded'; import NavPageLink from './NavPageLink'; import NERDrawer from '../../components/NERDrawer'; import NavUserMenu from '../PageTitle/NavUserMenu'; @@ -99,6 +100,11 @@ const Sidebar = ({ drawerOpen, setDrawerOpen, moveContent, setMoveContent }: Sid icon: , route: routes.RETROSPECTIVE }, + { + name: 'Rules', + icon: , + route: routes.RULES + }, { name: 'Info', icon: , diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectRules/AddRuleModal.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectRules/AddRuleModal.tsx new file mode 100644 index 0000000000..03745e7b78 --- /dev/null +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectRules/AddRuleModal.tsx @@ -0,0 +1,249 @@ +/* + * This file is part of NER's FinishLine and licensed under GNU AGPLv3. + * See the LICENSE file in the repository root folder for details. + */ + +import { useState, useMemo } from 'react'; +import { + Box, + Typography, + CircularProgress, + Alert, + FormControl, + Select, + MenuItem, + SelectChangeEvent, + IconButton, + useTheme +} from '@mui/material'; +import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import { Rule } from 'shared'; +import NERModal from '../../../../components/NERModal'; +import { useUnassignedRulesForRuleset } from '../../../../hooks/rules.hooks'; + +interface AddRuleModalProps { + open: boolean; + onHide: () => void; + rulesetId: string; + teamId: string; + onSubmit: (ruleIds: string[]) => void; +} + +const AddRuleModal = ({ open, onHide, rulesetId, teamId, onSubmit }: AddRuleModalProps) => { + const theme = useTheme(); + const [selectedRuleIds, setSelectedRuleIds] = useState([]); + + const { data: unassignedRules, isLoading, isError } = useUnassignedRulesForRuleset(rulesetId, teamId); + + type ParentInfo = { ruleId: string; ruleCode: string }; + + const uniqueParents = useMemo(() => { + if (!unassignedRules) return []; + const parentMap = new Map(); + unassignedRules.forEach((rule: Rule) => { + if (rule.parentRule) { + parentMap.set(rule.parentRule.ruleId, rule.parentRule); + } + }); + return Array.from(parentMap.values()).sort((a, b) => a.ruleCode.localeCompare(b.ruleCode)); + }, [unassignedRules]); + + const [selectedParentId, setSelectedParentId] = useState(''); + + const availableRules = useMemo(() => { + if (!unassignedRules || !selectedParentId) return []; + return unassignedRules.filter((rule: Rule) => rule.parentRule?.ruleId === selectedParentId); + }, [unassignedRules, selectedParentId]); + + const handleParentChange = (event: SelectChangeEvent) => { + setSelectedParentId(event.target.value); + setSelectedRuleIds([]); + }; + + const handleRuleSelect = (event: SelectChangeEvent) => { + const ruleId = event.target.value; + if (ruleId && !selectedRuleIds.includes(ruleId)) { + setSelectedRuleIds((prev) => [...prev, ruleId]); + } + }; + + const handleRemoveRule = (ruleId: string) => { + setSelectedRuleIds((prev) => prev.filter((id) => id !== ruleId)); + }; + + const handleSubmit = () => { + onSubmit(selectedRuleIds); + resetForm(); + onHide(); + }; + + const handleClose = () => { + resetForm(); + onHide(); + }; + + const resetForm = () => { + setSelectedRuleIds([]); + setSelectedParentId(''); + }; + + // Get rule display name + const getRuleName = (ruleId: string): string => { + const rule = unassignedRules?.find((r: Rule) => r.ruleId === ruleId); + return rule ? rule.ruleCode : ruleId; + }; + + // Dropdown styling + const selectStyles = { + backgroundColor: theme.palette.action.hover, + borderRadius: '8px', + color: theme.palette.text.primary, + '& .MuiSelect-select': { + py: 1.5, + px: 2.5 + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none' + }, + '& .MuiSvgIcon-root': { + color: theme.palette.text.primary + } + }; + + const labelStyles = { + color: theme.palette.primary.main, + textDecoration: 'underline', + fontSize: '2rem', + mb: '10px' + }; + + // Selected rule row styling + const selectedRuleStyles = { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: theme.palette.action.hover, + borderRadius: '8px', + px: 2.5, + py: 1.5, + mb: 1.5 + }; + + return ( + + + {isLoading ? ( + + + + ) : isError ? ( + Failed to load rules + ) : !unassignedRules || unassignedRules.length === 0 ? ( + + No unassigned rules available for this team. + + ) : ( + + {/* Select Section */} + + + Select Section + + + + + + + {/* Select Rules */} + + + Select Rules + + + {/* Selected Rules */} + {selectedRuleIds.map((ruleId) => ( + + + handleRemoveRule(ruleId)} + sx={{ color: theme.palette.text.primary, p: 0.5, mr: 1 }} + > + + + {getRuleName(ruleId)} + + + + ))} + + {/* Add Subtask dropdown */} + + + + + + )} + + + ); +}; + +export default AddRuleModal; diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectRules/ProjectRulesTab.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectRules/ProjectRulesTab.tsx new file mode 100644 index 0000000000..e29bfeef92 --- /dev/null +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectRules/ProjectRulesTab.tsx @@ -0,0 +1,447 @@ +/* + * This file is part of NER's FinishLine and licensed under GNU AGPLv3. + * See the LICENSE file in the repository root folder for details. + */ + +import { useState, useMemo } from 'react'; +import { + Box, + Button, + Typography, + CircularProgress, + Alert, + Tab, + Tabs as MuiTabs, + Table, + TableBody, + TableContainer, + Paper, + useTheme, + IconButton +} from '@mui/material'; +import { Project, ProjectRule, Rule, RuleCompletion } from 'shared'; +import LoadingIndicator from '../../../../components/LoadingIndicator'; +import ErrorPage from '../../../ErrorPage'; +import RuleRow from '../../../RulesPage/RuleRow'; +import UpdateStatusPopover from './UpdateStatusPopover'; +import AddRuleModal from './AddRuleModal'; +import { + useAllRulesetTypes, + useActiveRuleset, + useProjectRules, + useEditProjectRuleStatus, + useCreateProjectRule +} from '../../../../hooks/rules.hooks'; +import { useToast } from '../../../../hooks/toasts.hooks'; +import { InfoOutlined } from '@mui/icons-material'; +import { RuleHistoryModal } from './RuleHistoryModal'; + +interface ProjectRulesTabProps { + project: Project; +} + +/** + * Get the status chip configuration + */ +const getStatusConfig = (status: RuleCompletion) => { + switch (status) { + case RuleCompletion.COMPLETED: + return { label: 'Complete', color: '#4caf50' }; + case RuleCompletion.INCOMPLETE: + return { label: 'Incomplete', color: '#f44336' }; + case RuleCompletion.REVIEW: + default: + return { label: 'Review', color: '#ff9800' }; + } +}; + +export const ProjectRulesTab = ({ project }: ProjectRulesTabProps) => { + const toast = useToast(); + const theme = useTheme(); + + // State for modals and popovers + const [selectedRulesetTypeIndex, setSelectedRulesetTypeIndex] = useState(0); + const [statusPopoverAnchor, setStatusPopoverAnchor] = useState(null); + const [addRuleModalOpen, setAddRuleModalOpen] = useState(false); + const [selectedProjectRule, setSelectedProjectRule] = useState(null); + + const [selectedRuleForHistory, setSelectedRuleForHistory] = useState(null); + const [showHistoryModal, setShowHistoryModal] = useState(false); + + // Fetch all ruleset types + const { data: rulesetTypes, isLoading: rulesetTypesLoading, isError: rulesetTypesError } = useAllRulesetTypes(); + + // Get the currently selected ruleset type + const selectedRulesetType = rulesetTypes?.[selectedRulesetTypeIndex]; + + // Fetch the active ruleset for the selected ruleset type + const { data: activeRuleset, isLoading: activeRulesetLoading } = useActiveRuleset( + selectedRulesetType?.rulesetTypeId || '' + ); + + // Fetch project rules for the active ruleset + const { + data: projectRules, + isLoading: projectRulesLoading, + isError: projectRulesError + } = useProjectRules(activeRuleset?.rulesetId || '', project.id); + + // Mutations + const { mutateAsync: editStatusMutation, isLoading: isUpdatingStatus } = useEditProjectRuleStatus( + activeRuleset?.rulesetId || '', + project.id + ); + + const { mutateAsync: createProjectRuleMutation, isLoading: isCreating } = useCreateProjectRule(); + + // Get the first team's ID for fetching unassigned rules + const teamId = project.teams[0]?.teamId || ''; + + // Convert project rules to rules + const allRules = useMemo(() => { + if (!projectRules) return []; + return projectRules.map((pr) => pr.rule); + }, [projectRules]); + + // Get top-level rules (rules without a parent) + const topLevelRules = useMemo(() => { + return allRules.filter((rule) => !rule.parentRule); + }, [allRules]); + + // Helper function to get all descendant leaf rules for a given rule + const getDescendantLeafRules = (rule: Rule): Rule[] => { + const children = allRules.filter((r) => r.parentRule?.ruleId === rule.ruleId); + if (children.length === 0) { + // This is a leaf rule + return [rule]; + } + // Recursively get leaf rules from all children + return children.flatMap((child) => getDescendantLeafRules(child)); + }; + + // Helper function to calculate aggregated status from leaf rules + const getAggregatedStatus = (rule: Rule): RuleCompletion => { + const leafRules = getDescendantLeafRules(rule); + if (leafRules.length === 0) { + return RuleCompletion.REVIEW; + } + + const leafStatuses = leafRules.map((leafRule) => { + const projectRule = projectRules?.find((pr) => pr.rule.ruleId === leafRule.ruleId); + return projectRule?.currentStatus || RuleCompletion.REVIEW; + }); + + if (leafStatuses.every((s) => s === RuleCompletion.COMPLETED)) { + return RuleCompletion.COMPLETED; + } + + if (leafStatuses.some((s) => s === RuleCompletion.INCOMPLETE)) { + return RuleCompletion.INCOMPLETE; + } + + return RuleCompletion.REVIEW; + }; + + // Handle status update + const handleStatusUpdate = async (projectRuleId: string, newStatus: RuleCompletion) => { + try { + await editStatusMutation({ projectRuleId, newStatus }); + toast.success('Rule status updated successfully'); + } catch (error) { + if (error instanceof Error) { + toast.error(error.message); + } + } + }; + + // Handle add rules + const handleAddRules = async (ruleIds: string[]) => { + try { + for (const ruleId of ruleIds) { + await createProjectRuleMutation({ ruleId, projectId: project.id }); + } + toast.success(`${ruleIds.length} rule${ruleIds.length !== 1 ? 's' : ''} added successfully`); + } catch (error) { + if (error instanceof Error) { + toast.error(error.message); + } + } + }; + + // Handle opening status popover + const handleStatusClick = (event: React.MouseEvent, rule: Rule) => { + const projectRule = projectRules?.find((pr) => pr.rule.ruleId === rule.ruleId); + if (projectRule) { + // Only allow status updates for leaf rules + const hasChildren = allRules.some((r) => r.parentRule?.ruleId === rule.ruleId); + if (!hasChildren) { + setSelectedProjectRule(projectRule); + setStatusPopoverAnchor(event.currentTarget); + } + } + }; + + // Handle closing status popover + const handleStatusPopoverClose = () => { + setStatusPopoverAnchor(null); + setSelectedProjectRule(null); + }; + + // Handle tab change + const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { + setSelectedRulesetTypeIndex(newValue); + }; + + // Loading state + if (rulesetTypesLoading) { + return ; + } + + // Error state + if (rulesetTypesError) { + return ; + } + + // No ruleset types + if (!rulesetTypes || rulesetTypes.length === 0) { + return ( + + + No ruleset types configured for this organization. + + + ); + } + + // Check if we have no active ruleset + const hasNoActiveRuleset = !activeRulesetLoading && !activeRuleset; + + // Right content for rule rows - status badge + const renderRightContent = (rule: Rule) => { + const hasChildren = allRules.some((r) => r.parentRule?.ruleId === rule.ruleId); + const isLeafRule = !hasChildren; + + // Get status - for leaf rules use their own status, for parents calculate from children + const status = isLeafRule + ? projectRules?.find((pr) => pr.rule.ruleId === rule.ruleId)?.currentStatus || RuleCompletion.REVIEW + : getAggregatedStatus(rule); + const statusConfig = getStatusConfig(status); + + const projectRule = projectRules?.find((pr) => pr.rule.ruleId === rule.ruleId); + + return ( + <> + ) => { + e.stopPropagation(); + handleStatusClick(e, rule); + } + : undefined + } + sx={{ + backgroundColor: statusConfig.color, + color: 'white', + fontSize: '11px', + fontWeight: 600, + px: 0.75, + py: 0.25, + borderRadius: '3px', + cursor: isLeafRule ? 'pointer' : 'default', + display: 'inline-flex', + alignItems: 'center', + whiteSpace: 'nowrap', + '&:hover': isLeafRule + ? { + opacity: 0.85 + } + : {} + }} + > + {statusConfig.label} + + {isLeafRule && projectRule && projectRule.statusHistory && projectRule.statusHistory.length > 0 && ( + { + e.stopPropagation(); + setSelectedRuleForHistory(rule); + setShowHistoryModal(true); + }} + sx={{ + padding: '2px', + color: 'text.secondary', + '&:hover': { + color: 'primary.main' + } + }} + > + + + )} + + ); + }; + + const tableBackgroundColor = theme.palette.background.paper; + const tableTextColor = theme.palette.text.primary; + const tableHoverColor = theme.palette.action.hover; + + return ( + + {/* Ruleset Type Tabs */} + + + {rulesetTypes.map((rulesetType, idx) => ( + + ))} + + + + {/* Rules Content */} + {activeRulesetLoading || projectRulesLoading ? ( + + + + ) : hasNoActiveRuleset ? ( + + + No active ruleset configured for this ruleset type. + + + ) : projectRulesError ? ( + Failed to load rules + ) : topLevelRules.length === 0 ? ( + + + No rules assigned to this project yet. + + + ) : ( + + + + + {topLevelRules.map((rule) => ( + + ))} + +
+
+
+ )} + + {/* Add Rule Button */} + + + + + + + + {/* Update Status Popover */} + {selectedProjectRule && ( + + )} + + { + setShowHistoryModal(false); + setSelectedRuleForHistory(null); + }} + rule={selectedRuleForHistory} + projectRules={projectRules} + /> + + {/* Add Rule Modal */} + {activeRuleset && teamId && ( + setAddRuleModalOpen(false)} + rulesetId={activeRuleset.rulesetId} + teamId={teamId} + onSubmit={handleAddRules} + /> + )} + + {/* Loading overlay */} + {(isUpdatingStatus || isCreating) && ( + + + + )} + + ); +}; diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectRules/RuleHistoryModal.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectRules/RuleHistoryModal.tsx new file mode 100644 index 0000000000..637a861c0a --- /dev/null +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectRules/RuleHistoryModal.tsx @@ -0,0 +1,101 @@ +import { Box, Typography } from '@mui/material'; +import NERModal from '../../../../components/NERModal'; +import NERFailButton from '../../../../components/NERFailButton'; +import { Rule, ProjectRule, RuleCompletion } from 'shared'; + +interface RuleHistoryModalProps { + open: boolean; + onClose: () => void; + rule: Rule | null; + projectRules?: ProjectRule[]; +} + +/** + * Get the status chip configuration + */ +const getStatusConfig = (status: RuleCompletion) => { + switch (status) { + case RuleCompletion.COMPLETED: + return { label: 'Complete', color: '#4caf50' }; + case RuleCompletion.INCOMPLETE: + return { label: 'Incomplete', color: '#f44336' }; + case RuleCompletion.REVIEW: + default: + return { label: 'Review', color: '#ff9800' }; + } +}; + +export const RuleHistoryModal = ({ open, onClose, rule, projectRules }: RuleHistoryModalProps) => { + if (!rule) return null; + + const projectRule = projectRules?.find((pr) => pr.rule.ruleId === rule.ruleId); + const statusHistory = projectRule?.statusHistory || []; + + const formatDate = (date: Date) => { + return new Intl.DateTimeFormat('en-US', { + month: 'numeric', + day: 'numeric', + year: 'numeric' + }).format(date); + }; + + const formatUserName = (user: { firstName: string; lastName: string }) => { + return `${user.firstName} ${user.lastName}`; + }; + + const getStatusLabel = (status: RuleCompletion) => { + const config = getStatusConfig(status); + return config.label; + }; + + return ( + + + + + {statusHistory.map((history) => ( + + + •{formatDate(history.dateCreated)} - {formatUserName(history.createdBy)} Marked as{' '} + {getStatusLabel(history.newStatus)} + + + ))} + + + + Exit + + + + ); +}; diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectRules/UpdateStatusPopover.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectRules/UpdateStatusPopover.tsx new file mode 100644 index 0000000000..ff5f877ed6 --- /dev/null +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectRules/UpdateStatusPopover.tsx @@ -0,0 +1,81 @@ +/* + * This file is part of NER's FinishLine and licensed under GNU AGPLv3. + * See the LICENSE file in the repository root folder for details. + */ + +import { Box, Checkbox, FormControlLabel, Popover, Typography } from '@mui/material'; +import { ProjectRule, RuleCompletion } from 'shared'; + +interface UpdateStatusPopoverProps { + anchorEl: HTMLElement | null; + onClose: () => void; + projectRule: ProjectRule; + onStatusChange: (projectRuleId: string, newStatus: RuleCompletion) => void; +} + +const UpdateStatusPopover = ({ anchorEl, onClose, projectRule, onStatusChange }: UpdateStatusPopoverProps) => { + const open = Boolean(anchorEl); + + const handleStatusChange = (status: RuleCompletion) => { + onStatusChange(projectRule.projectRuleId, status); + onClose(); + }; + + const statusOptions = [ + { value: RuleCompletion.COMPLETED, label: 'Completed' }, + { value: RuleCompletion.INCOMPLETE, label: 'Incomplete' } + ]; + + return ( + + + {statusOptions.map((option) => ( + handleStatusChange(option.value)} + sx={{ + color: 'white', + '&.Mui-checked': { + color: 'white' + }, + p: 0.5 + }} + /> + } + label={{option.label}} + sx={{ + display: 'flex', + m: 0, + py: 0.5 + }} + /> + ))} + + + ); +}; + +export default UpdateStatusPopover; diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectViewContainer.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectViewContainer.tsx index 82ca5e59d7..38458cacee 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectViewContainer.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectViewContainer.tsx @@ -35,6 +35,7 @@ import ChangeRequestTab from '../../../components/ChangeRequestTab'; import PartsReviewPage from './PartReview/PartsReviewPage'; import ActionsMenu from '../../../components/ActionsMenu'; import { useMyTeamAsHead } from '../../../hooks/teams.hooks'; +import { ProjectRulesTab } from './ProjectRules/ProjectRulesTab'; interface ProjectViewContainerProps { project: Project; @@ -193,7 +194,8 @@ const ProjectViewContainer: React.FC = ({ project, en { tabUrlValue: 'changes', tabName: 'Changes' }, { tabUrlValue: 'gantt', tabName: 'Gantt' }, { tabUrlValue: 'change-requests', tabName: 'Change Requests' }, - { tabUrlValue: 'parts-review', tabName: 'Parts Review' } + { tabUrlValue: 'parts-review', tabName: 'Parts Review' }, + { tabUrlValue: 'rules', tabName: 'Rules' } ]} baseUrl={`${routes.PROJECTS}/${wbsNum}`} defaultTab="overview" @@ -216,8 +218,10 @@ const ProjectViewContainer: React.FC = ({ project, en ) : tab === 6 ? ( - ) : ( + ) : tab === 7 ? ( + ) : ( + )} {deleteModalShow && ( diff --git a/src/frontend/src/pages/RulesPage/AssignRulesTab.tsx b/src/frontend/src/pages/RulesPage/AssignRulesTab.tsx new file mode 100644 index 0000000000..42c6b7ec53 --- /dev/null +++ b/src/frontend/src/pages/RulesPage/AssignRulesTab.tsx @@ -0,0 +1,344 @@ +/* + * This file is part of NER's FinishLine and licensed under GNU AGPLv3. + * See the LICENSE file in the repository root folder for details. + */ + +import { + Box, + Chip, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, + Typography, + useTheme +} from '@mui/material'; +import { useState, useEffect } from 'react'; +import { Rule, TeamPreview } from 'shared'; +import { useAllTeams } from '../../hooks/teams.hooks'; +import LoadingIndicator from '../../components/LoadingIndicator'; +import ErrorPage from '../ErrorPage'; +import { useHistory, useParams } from 'react-router-dom'; +import { routes } from '../../utils/routes'; +import { useToast } from '../../hooks/toasts.hooks'; +import { NERButton } from '../../components/NERButton'; +import RuleRow from './RuleRow'; +import { useBulkToggleRuleTeam } from '../../hooks/rules.hooks'; + +/* + * Props for the assign rules tab. + */ +interface AssignRulesTabProps { + rules: Rule[]; +} + +const getLeafRuleIds = (ruleId: string, allRules: Rule[]): string[] => { + const rule = allRules.find((r) => r.ruleId === ruleId); + if (!rule) { + return []; + } + + if (rule.subRuleIds.length === 0) { + return [ruleId]; + } + + return rule.subRuleIds.flatMap((subId) => getLeafRuleIds(subId, allRules)); +}; + +/* + * Props for the team row. + */ +interface TeamRowProps { + team: TeamPreview; + isSelected: boolean; + onClick: () => void; +} + +/** + * Row component for displaying a team in the teams table. + */ +const TeamRow: React.FC = ({ team, isSelected, onClick }) => { + return ( + + + {team.teamName} + + + ); +}; + +/** + * Tab component for assigning rules to teams. + * Displays teams and rules side-by-side for selection. + */ +const AssignRulesTab: React.FC = ({ rules }) => { + const theme = useTheme(); + const history = useHistory(); + const { rulesetId } = useParams<{ rulesetId: string }>(); + const toast = useToast(); + const [selectedTeamId, setSelectedTeamId] = useState(null); + const [assignments, setAssignments] = useState>(new Set()); + const [originalAssignments, setOriginalAssignments] = useState>(new Set()); + const [isInitialized, setIsInitialized] = useState(false); + + const { data: teams, isLoading: teamsLoading, isError: teamsError, error: teamsErrorData } = useAllTeams(); + const { mutate: bulkToggle, isLoading: isSaving } = useBulkToggleRuleTeam(); + + // Load initial team assignments from rule data + useEffect(() => { + if (isInitialized || !teams || teams.length === 0) return; + + const initialAssignments = new Set(); + rules.forEach((rule) => { + rule.teams?.forEach((team) => { + initialAssignments.add(`${team.teamId}:${rule.ruleId}`); + }); + }); + + setOriginalAssignments(initialAssignments); + setAssignments(new Set(initialAssignments)); + setIsInitialized(true); + }, [rules, teams, isInitialized]); + + const handleTeamSelect = (teamId: string) => setSelectedTeamId(teamId); + + const isRuleAssigned = (ruleId: string) => { + if (!selectedTeamId) return false; + return assignments.has(`${selectedTeamId}:${ruleId}`); + }; + + const getAssignedTeamNames = (ruleId: string): string[] => { + if (!teams) return []; + const assignedTeamIds = [...assignments].filter((key) => key.endsWith(`:${ruleId}`)).map((key) => key.split(':')[0]); + return teams.filter((t) => assignedTeamIds.includes(t.teamId)).map((t) => t.teamName); + }; + + const renderTeamTags = (ruleId: string) => { + const teamNames = getAssignedTeamNames(ruleId); + if (teamNames.length === 0) return null; + return ( + + {teamNames.map((name) => ( + + ))} + + ); + }; + + const handleRuleToggle = (ruleId: string) => { + if (!selectedTeamId) { + toast.error('Please select a team first'); + return; + } + + const leafIds = getLeafRuleIds(ruleId, rules); + if (leafIds.length === 0) { + return; + } + + const newAssignments = new Set(assignments); + let allSelected = true; + for (const id of leafIds) { + if (!newAssignments.has(`${selectedTeamId}:${id}`)) { + allSelected = false; + break; + } + } + + for (const id of leafIds) { + const key = `${selectedTeamId}:${id}`; + if (allSelected) { + newAssignments.delete(key); + } else { + newAssignments.add(key); + } + } + + setAssignments(newAssignments); + }; + + const handleSaveAndExit = () => { + const toAdd = [...assignments].filter((key) => !originalAssignments.has(key)); + const toRemove = [...originalAssignments].filter((key) => !assignments.has(key)); + + if (toAdd.length === 0 && toRemove.length === 0) { + toast.info('No changes to save'); + return; + } + + // Build array of toggles to execute + const toggles: Array<{ ruleId: string; teamId: string }> = []; + + toAdd.forEach((key) => { + const [teamId, ruleId] = key.split(':'); + toggles.push({ ruleId, teamId }); + }); + + toRemove.forEach((key) => { + const [teamId, ruleId] = key.split(':'); + toggles.push({ ruleId, teamId }); + }); + + // Execute bulk toggle and navigate on success + bulkToggle(toggles, { + onSuccess: () => { + history.push(routes.RULESET_EDIT.replace(':rulesetId', rulesetId)); + } + }); + }; + + if (teamsLoading) { + return ; + } + + if (teamsError) { + return ; + } + + const topLevelRules = rules.filter((rule) => !rule.parentRule); + + return ( + + + {/* Teams Column */} + + + Teams: + + + + + {teams?.map((team) => ( + handleTeamSelect(team.teamId)} + /> + ))} + +
+
+
+ + {/* Rules Column */} + + + Rules: + + + + + {topLevelRules.map((rule) => ( + { + const leafIds = getLeafRuleIds(r.ruleId, rules); + const isSelected = leafIds.length > 0 && leafIds.every((id) => isRuleAssigned(id)); + return isSelected ? '#b36b6b' : '#CECECE'; + }} + hoverColor={(r) => { + const leafIds = getLeafRuleIds(r.ruleId, rules); + const isSelected = leafIds.length > 0 && leafIds.every((id) => isRuleAssigned(id)); + return isSelected ? '#a05858' : '#5e5e5e'; + }} + textColor="#000000" + onRowClick={(r) => handleRuleToggle(r.ruleId)} + middleContent={() => null} + rightContent={(r) => renderTeamTags(r.ruleId)} + verticalPadding="8px" + leftWidth="70%" + middleWidth="0%" + rightWidth="30%" + /> + ))} + +
+
+
+
+ + {/* Save & Exit Button */} + + + + + {isSaving ? 'Saving...' : 'Save & Exit'} + + + + + ); +}; + +export default AssignRulesTab; diff --git a/src/frontend/src/pages/RulesPage/RuleActions.tsx b/src/frontend/src/pages/RulesPage/RuleActions.tsx new file mode 100644 index 0000000000..a8ea66c07a --- /dev/null +++ b/src/frontend/src/pages/RulesPage/RuleActions.tsx @@ -0,0 +1,62 @@ +/* + * This file is part of NER's FinishLine and licensed under GNU AGPLv3. + * See the LICENSE file in the repository root folder for details. + */ + +import { Box, IconButton } from '@mui/material'; +import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; +import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline'; +import EditIcon from '@mui/icons-material/Edit'; + +interface RuleActionsProps { + ruleId: string; + onAdd: (ruleId: string, anchorEl: HTMLElement) => void; + onRemove: (ruleId: string) => void; + onEdit: (ruleId: string) => void; + iconColor?: string; +} + +/** + * RuleActions component for displaying actions for a rule. + * Supports adding, removing, and editing a rule. + */ +const RuleActions: React.FC = ({ ruleId, onAdd, onRemove, onEdit, iconColor = '#000000' }) => { + return ( + + { + e.stopPropagation(); + onAdd(ruleId, e.currentTarget); + }} + sx={{ padding: 0.25, color: iconColor }} + > + + + + { + e.stopPropagation(); + onRemove(ruleId); + }} + sx={{ padding: 0.25, color: iconColor }} + > + + + + { + e.stopPropagation(); + onEdit(ruleId); + }} + sx={{ padding: 0.25, color: iconColor }} + > + + + + ); +}; + +export default RuleActions; diff --git a/src/frontend/src/pages/RulesPage/RuleRow.tsx b/src/frontend/src/pages/RulesPage/RuleRow.tsx new file mode 100644 index 0000000000..2904a5ed3e --- /dev/null +++ b/src/frontend/src/pages/RulesPage/RuleRow.tsx @@ -0,0 +1,195 @@ +/* + * This file is part of NER's FinishLine and licensed under GNU AGPLv3. + * See the LICENSE file in the repository root folder for details. + */ + +import { TableCell, TableRow, Box } from '@mui/material'; +import { useState } from 'react'; +import { Rule } from 'shared'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import { useGetChildRules } from '../../hooks/rules.hooks'; + +interface RuleRowProps { + rule: Rule; + allRules?: Rule[]; + level?: number; + leftContent?: (rule: Rule, level: number, isExpanded: boolean, hasSubRules: boolean) => React.ReactNode; + middleContent?: (rule: Rule, level: number) => React.ReactNode; + rightContent: (rule: Rule, level: number) => React.ReactNode; + backgroundColor: string | ((rule: Rule) => string); + textColor: string | ((rule: Rule) => string); + hoverColor: string | ((rule: Rule) => string); + onRowClick?: (rule: Rule) => void; + rowHeight?: string; + verticalPadding?: string; + horizontalPadding?: string; + leftWidth?: string; + middleWidth?: string; + rightWidth?: string; + initiallyExpanded?: boolean; +} + +/** + * Recursive component for rendering a rule row in a rules table. + * Supports expand/collapsing of rules with sub-rules. + */ +const RuleRow: React.FC = ({ + rule, + allRules, + level = 0, + leftContent, + middleContent, + rightContent, + backgroundColor, + textColor, + hoverColor, + onRowClick, + rowHeight, + verticalPadding = '12px', + horizontalPadding = '16px', + leftWidth = '20%', + middleWidth = '70%', + rightWidth = '10%', + initiallyExpanded = false +}) => { + const [isExpanded, setIsExpanded] = useState(initiallyExpanded); + const hasSubRules = rule.subRuleIds.length > 0; + + // Lazy load if allRules not provided + const { data: fetchedSubRules = [] } = useGetChildRules(rule.ruleId, !allRules && isExpanded && hasSubRules); + + // Use allRules if provided, otherwise use fetched + const subRules = allRules ? allRules.filter((r) => rule.subRuleIds.includes(r.ruleId)) : fetchedSubRules; + + const bgColor = typeof backgroundColor === 'function' ? backgroundColor(rule) : backgroundColor; + const color = typeof textColor === 'function' ? textColor(rule) : textColor; + const hoverBgColor = typeof hoverColor === 'function' ? hoverColor(rule) : hoverColor; + + const toggleExpand = () => hasSubRules && setIsExpanded(!isExpanded); + + const handleChevronClick = (e: React.MouseEvent) => { + e.stopPropagation(); + toggleExpand(); + }; + + const handleRowClick = () => { + if (onRowClick) { + onRowClick(rule); + } + }; + + const commonCellStyles = { + fontSize: '16px', + padding: `${verticalPadding} ${horizontalPadding}`, + backgroundColor: 'inherit', + borderBottom: 'none', + height: rowHeight + }; + + const defaultLeftContent = ( + + {hasSubRules && ( + + )} + {rule.ruleCode} + + ); + + return ( + <> + + + {leftContent ? leftContent(rule, level, isExpanded, hasSubRules) : defaultLeftContent} + + + {middleContent + ? middleContent(rule, level) + : rule.ruleContent && {rule.ruleContent}} + + + {rightContent(rule, level)} + + + {isExpanded && + hasSubRules && + subRules.map((subRule) => ( + + ))} + + ); +}; + +export default RuleRow; diff --git a/src/frontend/src/pages/RulesPage/Rules.tsx b/src/frontend/src/pages/RulesPage/Rules.tsx new file mode 100644 index 0000000000..3ac570f131 --- /dev/null +++ b/src/frontend/src/pages/RulesPage/Rules.tsx @@ -0,0 +1,20 @@ +// switch route page for rules +import { Route, Switch } from 'react-router-dom'; +import { routes } from '../../utils/routes'; +import RulesetTypePage from './RulesetTypePage'; +import RulesetPage from './RulesetPage'; +import RulesetEditPage from './RulesetEditPage'; +import RulesetViewPage from './RulesetViewPage'; + +const RulesPage: React.FC = () => { + return ( + + + + + + + ); +}; + +export default RulesPage; diff --git a/src/frontend/src/pages/RulesPage/RulesetEditPage.tsx b/src/frontend/src/pages/RulesPage/RulesetEditPage.tsx new file mode 100644 index 0000000000..bd7992f551 --- /dev/null +++ b/src/frontend/src/pages/RulesPage/RulesetEditPage.tsx @@ -0,0 +1,370 @@ +/* + * This file is part of NER's FinishLine and licensed under GNU AGPLv3. + * See the LICENSE file in the repository root folder for details. + */ + +import { Box, Button, Paper, Table, TableBody, TableContainer, TextField, useTheme } from '@mui/material'; +import { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import PageLayout from '../../components/PageLayout'; +import FullPageTabs from '../../components/FullPageTabs'; +import { routes } from '../../utils/routes'; +import RuleRow from './RuleRow'; +import RuleActions from './RuleActions'; +import ErrorPage from '../ErrorPage'; +import LoadingIndicator from '../../components/LoadingIndicator'; +import AddRuleSectionModal from './components/AddRuleSectionModal'; +import AddRuleModal from './components/AddRuleModal'; +import { AddRuleBox } from './components/AddRuleBox'; +import AssignRulesTab from './AssignRulesTab'; +import DeleteRuleModal from './components/DeleteRuleModal'; +import { useDeleteRule, useEditRule, useSingleRuleset, useAllRulesForRuleset } from '../../hooks/rules.hooks'; +import { countRulesToDelete } from '../../utils/rules.utils'; +import { Rule } from 'shared'; + +/** + * RulesetPage component for displaying and managing ruleset rules. + * Supports editing and assigning rules to projects and teams. + */ +const RulesetEditPage: React.FC = () => { + const { rulesetId } = useParams<{ rulesetId: string; tabValue?: string }>(); //why tab value?? + const [tabValue, setTabValue] = useState(0); + const defaultTab = 'edit-rules'; + + const [showAddMenu, setShowAddMenu] = useState(false); + const [addMenuAnchorEl, setAddMenuAnchorEl] = useState(null); + const [activeRuleId, setActiveRuleId] = useState(null); + + // temporary placeholder useState fns for the add rule section and add rule modals + const [showAddRuleSectionModal, setShowAddRuleSectionModal] = useState(false); + const [showAddRuleModal, setShowAddRuleModal] = useState(false); + + // Delete modal state + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [ruleToDelete, setRuleToDelete] = useState(null); + + // Editing state + const [editingRuleId, setEditingRuleId] = useState(null); + const [editedContent, setEditedContent] = useState(''); + + const theme = useTheme(); + + const { + data: ruleset, + isError: isRulesetError, + error: rulesetError, + isLoading: isRulesetLoading + } = useSingleRuleset(rulesetId!); + const { + data: allRules, + isError: isRulesError, + error: rulesError, + isLoading: isRulesLoading + } = useAllRulesForRuleset(rulesetId!); + const { mutateAsync: deleteRuleMutation } = useDeleteRule(); + const { mutateAsync: editRuleMutation } = useEditRule(); + + const tabs = [ + { tabUrlValue: 'edit-rules', tabName: 'Edit Rules' }, + { tabUrlValue: 'assign-rules', tabName: 'Assign Rules' } + ]; + + if (isRulesetError) { + return ; + } + + if (isRulesError) { + return ; + } + + if (isRulesetLoading || isRulesLoading || !ruleset || !allRules) { + return ; + } + + const handleAddRuleSection = () => { + setShowAddRuleSectionModal(true); + }; + + const handleOpenAddMenu = (ruleId: string, anchorEl: HTMLElement) => { + if (showAddMenu && addMenuAnchorEl === anchorEl) { + handleCloseAddMenu(); + return; + } + + setActiveRuleId(ruleId); + setAddMenuAnchorEl(anchorEl); + setShowAddMenu(true); + }; + + const handleCloseAddMenu = () => { + setShowAddMenu(false); + setAddMenuAnchorEl(null); + }; + + const handleAddRuleFromMenu = () => { + setShowAddRuleModal(true); + handleCloseAddMenu(); + }; + + const handleRemoveRule = (ruleId: string) => { + const rule = allRules.find((r) => r.ruleId === ruleId); + if (rule) { + setRuleToDelete(rule); + setDeleteModalOpen(true); + } + }; + + const handleDeleteConfirm = async () => { + if (!ruleToDelete) return; + + try { + await deleteRuleMutation(ruleToDelete.ruleId); + setDeleteModalOpen(false); + setRuleToDelete(null); + } catch (err) { + console.error('Failed to delete rule:', err); + } + }; + + const handleDeleteCancel = () => { + setDeleteModalOpen(false); + setRuleToDelete(null); + }; + + const handleEditRule = (ruleId: string) => { + const rule = allRules.find((r) => r.ruleId === ruleId); + if (rule) { + setEditingRuleId(ruleId); + setEditedContent(rule.ruleContent); + } + }; + + const handleSaveEdit = async () => { + if (!editingRuleId) return; + + try { + await editRuleMutation({ ruleId: editingRuleId, ruleContent: editedContent }); + setEditingRuleId(null); + setEditedContent(''); + } catch (err) { + console.error('Failed to update rule:', err); + } + }; + + const handleCancelEdit = () => { + setEditingRuleId(null); + setEditedContent(''); + }; + + const totalRulesToDelete = ruleToDelete ? countRulesToDelete(ruleToDelete, allRules) : 0; + + // Filter to only show top-level rules + const topLevelRules = allRules.filter((rule) => !rule.parentRule); + + return ( + + +
+ } + > + + {tabValue === 0 ? ( + + + + + {topLevelRules.map((rule) => ( + { + const isEditing = editingRuleId === currentRule.ruleId; + if (isEditing) { + return ( + setEditedContent(e.target.value)} + variant="outlined" + size="small" + autoFocus + sx={{ + backgroundColor: theme.palette.grey[100], + '& .MuiOutlinedInput-root': { + color: '#000000', + '& fieldset': { + borderColor: '#dd514c' + }, + '&:hover fieldset': { + borderColor: '#dd514c' + }, + '&.Mui-focused fieldset': { + borderColor: '#dd514c' + } + } + }} + /> + ); + } + return ( + currentRule.ruleContent && {currentRule.ruleContent} + ); + }} + rightContent={(currentRule) => ( + + )} + backgroundColor={(currentRule) => (editingRuleId === currentRule.ruleId ? '#c0c0c0' : '#9d9d9d')} + textColor="#000000" + hoverColor="#5e5e5e" + rowHeight="10px" + verticalPadding="5px" + /> + ))} + +
+
+ + + + setShowAddRuleSectionModal(false)} + rulesetId={rulesetId} + /> + + setShowAddRuleModal(false)} + rulesetId={rulesetId} + initialParentRuleId={activeRuleId || undefined} + /> + + {ruleToDelete && ( + + )} + + + + + {editingRuleId ? ( + <> + + + + ) : ( + + )} + + + + ) : ( + + )} +
+ + ); +}; + +export default RulesetEditPage; diff --git a/src/frontend/src/pages/RulesPage/RulesetPage.tsx b/src/frontend/src/pages/RulesPage/RulesetPage.tsx new file mode 100644 index 0000000000..d3618acb02 --- /dev/null +++ b/src/frontend/src/pages/RulesPage/RulesetPage.tsx @@ -0,0 +1,128 @@ +/* + * This file is part of NER's FinishLine and licensed under GNU AGPLv3. + * See the LICENSE file in the repository root folder for details. + */ +import { useParams } from 'react-router-dom'; +import React from 'react'; +import { useToast } from '../../hooks/toasts.hooks'; +import { useCreateRuleset, useDeleteRuleset, useParseRuleset } from '../../hooks/rules.hooks'; +import { NERButton } from '../../components/NERButton'; +import AddNewFileModal from './components/AddNewFileModal'; +import PageLayout from '../../components/PageLayout'; +import { Box } from '@mui/material'; +import RulesetTable from './components/RulesetTable'; +import { routes } from '../../utils/routes'; +import { useRulesetType } from '../../hooks/rules.hooks'; +import LoadingIndicator from '../../components/LoadingIndicator'; +import ErrorPage from '../ErrorPage'; + +/** + * RulesetPage component for displaying and managing ruleset rules. + * Supports editing and assigning rules to projects and teams. + */ +const RulesetPage: React.FC = () => { + const { rulesetTypeId } = useParams<{ rulesetTypeId: string }>(); + + const { mutateAsync: createRuleset } = useCreateRuleset(); + const { mutateAsync: parseRuleset } = useParseRuleset(); + const { mutateAsync: deleteRuleset } = useDeleteRuleset(); + const toast = useToast(); + + const [AddFileModalShow, setAddFileModalShow] = React.useState(false); + const { data: rulesetType, isLoading, isError, error } = useRulesetType(rulesetTypeId); + + const handleFileConfirm = async (data: { fileId: string; name: string; carNumber: number; parserType: string }) => { + setAddFileModalShow(false); + toast.info('Creating ruleset and parsing rules...'); + + let createdRulesetId: string | null = null; + + try { + const ruleset = await createRuleset({ + fileId: data.fileId, + name: data.name, + rulesetTypeId, + carNumber: data.carNumber, + active: false + }); + const { rulesetId } = ruleset; + + if (!rulesetId) { + throw new Error('Error creating Ruleset'); + } + + createdRulesetId = rulesetId; + + const parsedRules = await parseRuleset({ + rulesetId, + fileId: data.fileId, + parserType: data.parserType as 'FSAE' | 'FHE' + }); + toast.success(`Successfully parsed ${parsedRules.length} rules!`); + } catch (e) { + if (createdRulesetId) { + try { + await deleteRuleset(createdRulesetId); + toast.error('Parsing failed. Ruleset has been removed. ' + (e instanceof Error ? e.message : 'Unknown error')); + } catch (deleteError) { + toast.error('Error during cleanup: ' + (deleteError instanceof Error ? deleteError.message : 'Unknown error')); + } + } else { + toast.error('Error creating ruleset: ' + (e instanceof Error ? e.message : 'Unknown error')); + } + } + }; + + if (isLoading) return ; + if (isError) return ; + + return ( + <> + + + + + + + + + {/* Add New File Button */} + setAddFileModalShow(!AddFileModalShow)}> + Add New File + + setAddFileModalShow(false)} + onFormSubmit={handleFileConfirm} + /> + + + + + + ); +}; + +export default RulesetPage; diff --git a/src/frontend/src/pages/RulesPage/RulesetTypePage.tsx b/src/frontend/src/pages/RulesPage/RulesetTypePage.tsx new file mode 100644 index 0000000000..3259a9bb1c --- /dev/null +++ b/src/frontend/src/pages/RulesPage/RulesetTypePage.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import PageLayout from '../../components/PageLayout'; +import { Box } from '@mui/material'; +import RulesetTypeTable from './components/RulesetTypeTable'; +import { NERButton } from '../../components/NERButton'; +import AddRulesetTypeModal from './components/AddRulesetTypeModal'; +import { useState } from 'react'; +import { useCreateRulesetType } from '../../hooks/rules.hooks'; + +const RulesetTypePage: React.FC = () => { + const [addRulesetTypeModalShow, setAddRulesetTypeModalShow] = useState(false); + + const { mutateAsync: createRulesetType } = useCreateRulesetType(); + + const handleAddRulesetTypeConfirm = async (data: { name: string }) => { + await createRulesetType({ name: data.name }); + }; + + const handleAddRulesetTypeCancel = () => { + setAddRulesetTypeModalShow(false); + }; + + return ( + <> + + + + + + + + + setAddRulesetTypeModalShow(!addRulesetTypeModalShow)}> + Add Ruleset Type + + + + + + + + ); +}; + +export default RulesetTypePage; diff --git a/src/frontend/src/pages/RulesPage/RulesetViewPage.tsx b/src/frontend/src/pages/RulesPage/RulesetViewPage.tsx new file mode 100644 index 0000000000..e1457510e8 --- /dev/null +++ b/src/frontend/src/pages/RulesPage/RulesetViewPage.tsx @@ -0,0 +1,129 @@ +import React, { useState } from 'react'; +import FullPageTabs from '../../components/FullPageTabs'; +import PageLayout from '../../components/PageLayout'; +import { routes } from '../../utils/routes'; +import { Box } from '@mui/system'; +import { useParams } from 'react-router-dom'; +import ErrorPage from '../ErrorPage'; +import LoadingIndicator from '../../components/LoadingIndicator'; +import RulesetGeneralView from './components/RulesetGeneralView'; +import { Rule } from 'shared'; +import RulesetTeamView, { TeamRules } from './components/RulesetTeamView'; +import { useSingleRuleset, useAllRulesForRuleset } from '../../hooks/rules.hooks'; + +/** + * Organizes rules by team and project assignments. + * Rules without team assignments are shown in the unassigned section. + */ +const getTeamOrganization = (allRules: Rule[]): { teamRules: TeamRules[]; unassignedToTeam: Rule[] } => { + const teamMap = new Map(); + const unassignedToTeam: Rule[] = []; + + // Iterate through all rules and organize by team + allRules.forEach((rule) => { + if (!rule.teams || rule.teams.length === 0) { + // Only add to unassigned if it's a top-level rule (no parent) + if (!rule.parentRule) { + unassignedToTeam.push(rule); + } + } else { + // Add rule to each assigned team (includes both parents and children) + rule.teams.forEach((team) => { + if (!teamMap.has(team.teamId)) { + teamMap.set(team.teamId, { + teamId: team.teamId, + teamName: team.teamName, + projects: [], + unassignedRules: [] + }); + } + + const teamRules = teamMap.get(team.teamId)!; + if (!rule.parentRule) { + teamRules.unassignedRules.push(rule); + } + }); + } + }); + + return { teamRules: Array.from(teamMap.values()), unassignedToTeam }; +}; + +const RulesetViewPage = () => { + const [tabIndex, setTabIndex] = useState(0); + const tabs = [ + { tabUrlValue: 'teamView', tabName: 'Team View' }, + { tabUrlValue: 'generalView', tabName: 'General View' } + ]; + + const { rulesetId } = useParams<{ rulesetId: string }>(); + + const { + data: ruleset, + isError: isRulesetError, + error: rulesetError, + isLoading: isRulesetLoading + } = useSingleRuleset(rulesetId!); + + const { + data: allRules, + isError: isRulesError, + error: rulesError, + isLoading: isRulesLoading + } = useAllRulesForRuleset(rulesetId!); + + if (isRulesetLoading || isRulesLoading) { + return ; + } + + if (isRulesetError) { + return ; + } + + if (isRulesError) { + return ; + } + + if (!ruleset || !allRules) { + return ; + } + + const { teamRules, unassignedToTeam } = getTeamOrganization(allRules); + + return ( + + + + + } + > + + {tabIndex === 0 ? ( + + ) : ( + + )} + + +
+ ); +}; + +export default RulesetViewPage; diff --git a/src/frontend/src/pages/RulesPage/components/AddNewFileModal.tsx b/src/frontend/src/pages/RulesPage/components/AddNewFileModal.tsx new file mode 100644 index 0000000000..8df5e0bd7d --- /dev/null +++ b/src/frontend/src/pages/RulesPage/components/AddNewFileModal.tsx @@ -0,0 +1,271 @@ +import NERFormModal from '../../../components/NERFormModal'; +import { useForm, Controller } from 'react-hook-form'; +import { Box, FormControl, TextField, Typography, FormLabel, FormHelperText, Button, Select, MenuItem } from '@mui/material'; +import { useEffect, useState } from 'react'; +import * as yup from 'yup'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { useToast } from '../../../hooks/toasts.hooks'; +import { FileUpload } from '@mui/icons-material'; +import { MAX_FILE_SIZE } from 'shared'; +import { useUploadRulesetFile } from '../../../hooks/rules.hooks'; +import { useGetAllCars } from '../../../hooks/cars.hooks'; + +interface AddNewFileModalProps { + open: boolean; + onHide: () => void; + onFormSubmit: (data: NewFileFormData) => Promise; +} + +interface NewFileFormData { + fileId: string; + name: string; + carNumber: number; + parserType: 'FSAE' | 'FHE'; +} + +interface ButtonGroupProps { + options: string[]; + value: string; + onChange: (value: string) => any; +} + +const sectionHeaderStyle = { + fontWeight: 'bold', + color: '#ef4345', + textDecoration: 'underline', + fontSize: '1rem', + textUnderlineOffset: '5px', + marginBottom: '10px' +}; + +const isPdf = (fileName: string) => { + const extension = fileName.split('.').pop()?.toLowerCase(); + return extension === 'pdf'; +}; + +const schema = yup.object({ + fileId: yup.string().required('File is required'), + name: yup.string().required('Name is required'), + carNumber: yup.number().min(0).required('Car is required'), + parserType: yup.string().oneOf(['FSAE', 'FHE']).required('Parser type is required') +}); + +const ButtonGroup: React.FC = ({ options, value, onChange }) => { + return ( +
+ {options.map((option) => ( + + ))} +
+ ); +}; + +const AddNewFileModal: React.FC = ({ open, onHide, onFormSubmit }) => { + const toast = useToast(); + const [file, setFile] = useState(null); + const [uploading, setUploading] = useState(false); + const { mutateAsync: uploadFile } = useUploadRulesetFile(); + const { data: cars, isLoading: carsLoading, isError: carsError } = useGetAllCars(); + + const { + formState: { errors }, + handleSubmit, + reset, + setValue, + control + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + fileId: '', + name: '', + carNumber: 100, + parserType: 'FSAE' + } + }); + + useEffect(() => { + if (cars && cars.length > 0) { + setValue('carNumber', cars[0].wbsNum.carNumber); + } + }, [cars, setValue]); + + const handleFormSubmit = async (data: NewFileFormData) => { + try { + await onFormSubmit(data); + setFile(null); + reset(); + onHide(); + } catch (error: unknown) { + if (error instanceof Error) { + toast.error(error.message); + } + } + }; + + const handleFileUpload = async (e: React.ChangeEvent) => { + if (!e.target.files || !e.target.files[0]) { + return; + } + + const [selectedFile] = e.target.files; + + if (!isPdf(selectedFile.name)) { + const error = 'File must be a PDF'; + toast.error(error); + return; + } + + if (selectedFile.size > MAX_FILE_SIZE) { + const error = `File exceeds the maximum size limit of ${MAX_FILE_SIZE / (1024 * 1024)} MB`; + toast.error(error); + return; + } + + setUploading(true); + + try { + const fileId = await uploadFile(selectedFile); + setValue('fileId', fileId, { shouldValidate: true }); + setFile(selectedFile); + toast.success('File uploaded successfully'); + } catch (error: unknown) { + let errorMessage = 'File upload failed. Please try again.'; + if (error instanceof Error) { + errorMessage = error.message || errorMessage; + } + toast.error(errorMessage); + setFile(null); + setValue('fileId', '', { shouldValidate: false }); + } finally { + setUploading(false); + } + }; + + const handleModalClose = () => { + setFile(null); + reset(); + onHide(); + }; + + const handleReset = () => { + setFile(null); + reset(); + }; + + return ( + + + + + {/* File Upload */} + + Upload Ruleset File: + + {file && {file.name}} + {uploading && Uploading...} + + + {errors.fileId?.message} + + + {/* Car */} + + Car: + {carsLoading ? ( + Loading cars... + ) : carsError ? ( + Failed to load cars + ) : ( + ( + + )} + /> + )} + {errors.carNumber?.message} + + + {/* Parser Type */} + + Parser Type: + ( + onChange(val as 'FSAE' | 'FHE')} /> + )} + /> + {errors.parserType?.message} + + + + {/* Ruleset Name */} + + Name Ruleset File: + ( + + )} + /> + {errors.name?.message} + + + + + ); +}; + +export default AddNewFileModal; diff --git a/src/frontend/src/pages/RulesPage/components/AddRuleBox.tsx b/src/frontend/src/pages/RulesPage/components/AddRuleBox.tsx new file mode 100644 index 0000000000..3bb9eea306 --- /dev/null +++ b/src/frontend/src/pages/RulesPage/components/AddRuleBox.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { Box, Popover, useTheme } from '@mui/material'; +import { NERButton } from '../../../components/NERButton'; + +type AddRuleBoxProps = { + open: boolean; + anchorEl: HTMLElement | null; + onClose: () => void; + onAddRule: () => void; +}; + +export const AddRuleBox: React.FC = ({ open, anchorEl, onClose, onAddRule }) => { + const theme = useTheme(); + + return ( + + + + + Add Rule + + + + ); +}; diff --git a/src/frontend/src/pages/RulesPage/components/AddRuleModal.tsx b/src/frontend/src/pages/RulesPage/components/AddRuleModal.tsx new file mode 100644 index 0000000000..830fcfddda --- /dev/null +++ b/src/frontend/src/pages/RulesPage/components/AddRuleModal.tsx @@ -0,0 +1,169 @@ +import { useState, useEffect } from 'react'; +import { Box, Typography, useTheme, TextField } from '@mui/material'; +import { useForm, Controller } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; +import NERFormModal from '../../../components/NERFormModal'; +import { useToast } from '../../../hooks/toasts.hooks'; +import { useCreateRule } from '../../../hooks/rules.hooks'; + +interface AddRuleModalProps { + open: boolean; + onClose: () => void; + rulesetId: string; + initialParentRuleId?: string; +} + +interface FormData { + ruleCode: string; + ruleContent: string; +} + +const schema = yup.object().shape({ + ruleCode: yup.string().required('Rule Code is required'), + ruleContent: yup.string().required('Rule Content is required') +}); + +const AddRuleModal: React.FC = ({ open, onClose, rulesetId, initialParentRuleId }) => { + const theme = useTheme(); + const toast = useToast(); + const { mutateAsync: createRule } = useCreateRule(); + + // Track the hierarchy of selected REFERENCED rules (separate from parent) + const [selectedReferenceHierarchy, setSelectedReferenceHierarchy] = useState([]); + + const { + handleSubmit, + control, + reset, + formState: { errors } + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + ruleCode: '', + ruleContent: '' + } + }); + + // Reset reference hierarchy when modal opens/closes or parent changes + useEffect(() => { + if (open) { + setSelectedReferenceHierarchy([]); + } + }, [open, initialParentRuleId]); + + const onSubmit = async (data: FormData) => { + try { + const referencedRules = + selectedReferenceHierarchy.length > 0 ? [selectedReferenceHierarchy[selectedReferenceHierarchy.length - 1]] : []; + + await createRule({ + ruleCode: data.ruleCode, + ruleContent: data.ruleContent, + rulesetId, + parentRuleId: initialParentRuleId, + referencedRules, + imageFileIds: [] + }); + + toast.success('Rule created successfully'); + handleClose(); + } catch (error) { + toast.error('Failed to create rule'); + } + }; + + const handleClose = () => { + setSelectedReferenceHierarchy([]); + onClose(); + }; + + const textFieldStyles = { + '& .MuiOutlinedInput-root': { + backgroundColor: theme.palette.action.hover, + borderRadius: '8px', + '& fieldset': { + border: 'none' + }, + '&:hover fieldset': { + border: 'none' + }, + '&.Mui-focused fieldset': { + border: 'none' + } + }, + '& .MuiInputBase-input': { + color: theme.palette.text.primary, + py: 1.5, + px: 2.5 + } + }; + + return ( + { + reset(); + setSelectedReferenceHierarchy([]); + }} + title="Add Rule" + handleUseFormSubmit={handleSubmit} + onFormSubmit={onSubmit} + formId="add-rule-form" + showCloseButton + > + + {/* Rule Code */} + + + Rule Code* + + ( + + )} + /> + + + {/* Rule Content */} + + + Rule Content* + + ( + + )} + /> + + + + ); +}; + +export default AddRuleModal; diff --git a/src/frontend/src/pages/RulesPage/components/AddRuleSectionModal.tsx b/src/frontend/src/pages/RulesPage/components/AddRuleSectionModal.tsx new file mode 100644 index 0000000000..3b21e2bf11 --- /dev/null +++ b/src/frontend/src/pages/RulesPage/components/AddRuleSectionModal.tsx @@ -0,0 +1,123 @@ +import { Box, Typography, useTheme, TextField } from '@mui/material'; +import { useForm, Controller } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; +import NERFormModal from '../../../components/NERFormModal'; +import { useToast } from '../../../hooks/toasts.hooks'; +import { useCreateRule } from '../../../hooks/rules.hooks'; + +interface AddRuleSectionModalProps { + open: boolean; + onClose: () => void; + rulesetId: string; +} + +interface FormData { + name: string; +} + +const schema = yup.object().shape({ + name: yup.string().required('Name is required') +}); + +const AddRuleSectionModal: React.FC = ({ open, onClose, rulesetId }) => { + const theme = useTheme(); + const toast = useToast(); + const { mutateAsync: createRule } = useCreateRule(); + + const { + handleSubmit, + control, + reset, + formState: { errors } + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + name: '' + } + }); + + const onSubmit = async (data: FormData) => { + try { + await createRule({ + ruleCode: data.name, + ruleContent: 'content placeholder', + rulesetId, + referencedRules: [], + imageFileIds: [] + }); + + toast.success('Rule section created successfully'); + handleClose(); + } catch (error) { + toast.error('Failed to create rule section'); + } + }; + + const handleClose = () => { + reset(); + onClose(); + }; + + const textFieldStyles = { + '& .MuiOutlinedInput-root': { + backgroundColor: theme.palette.action.hover, + borderRadius: '8px', + '& fieldset': { + border: 'none' + }, + '&:hover fieldset': { + border: 'none' + }, + '&.Mui-focused fieldset': { + border: 'none' + } + }, + '& .MuiInputBase-input': { + color: theme.palette.text.primary, + py: 1.5, + px: 2.5 + } + }; + + return ( + + + {/* Name Rule Section */} + + + Name Rule Section: + + ( + + )} + /> + + + + ); +}; + +export default AddRuleSectionModal; diff --git a/src/frontend/src/pages/RulesPage/components/AddRulesetTypeModal.tsx b/src/frontend/src/pages/RulesPage/components/AddRulesetTypeModal.tsx new file mode 100644 index 0000000000..0e9caf9c9c --- /dev/null +++ b/src/frontend/src/pages/RulesPage/components/AddRulesetTypeModal.tsx @@ -0,0 +1,104 @@ +import { FormControl, FormHelperText, FormLabel, TextField } from '@mui/material'; +import { Box } from '@mui/system'; +import * as yup from 'yup'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { Controller, useForm } from 'react-hook-form'; +import NERFormModal from '../../../components/NERFormModal'; +import { useToast } from '../../../hooks/toasts.hooks'; + +interface RulesetTypeFormData { + name: string; +} + +interface AddRulesetTypeModalProps { + open: boolean; + onHide: () => void; + onFormSubmit: (data: RulesetTypeFormData) => Promise; +} + +const sectionHeaderStyle = { + fontWeight: 'bold', + color: '#ef4345', + textDecoration: 'underline', + fontSize: '1rem', + textUnderlineOffset: '5px', + marginBottom: '10px' +}; + +const schema = yup.object({ + name: yup.string().required('Name is required') +}); + +const AddRulesetTypeModal: React.FC = ({ open, onHide, onFormSubmit }) => { + const toast = useToast(); + + const { + formState: { errors }, + handleSubmit, + reset, + control + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + name: '' + } + }); + + const handleFormSubmit = async (data: RulesetTypeFormData) => { + try { + await onFormSubmit(data); + toast.success('Ruleset Type Successfully Added'); + reset(); + onHide(); + } catch (error: unknown) { + if (error instanceof Error) { + toast.error(error.message); + } + } + }; + + const handleModalClose = () => { + reset(); + onHide(); + }; + + const handleReset = () => { + reset(); + }; + + return ( + + + + Name Ruleset: + ( + + )} + /> + {errors.name?.message} + + + + ); +}; + +export default AddRulesetTypeModal; diff --git a/src/frontend/src/pages/RulesPage/components/DeleteRuleModal.tsx b/src/frontend/src/pages/RulesPage/components/DeleteRuleModal.tsx new file mode 100644 index 0000000000..573ea843a6 --- /dev/null +++ b/src/frontend/src/pages/RulesPage/components/DeleteRuleModal.tsx @@ -0,0 +1,51 @@ +/* + * This file is part of NER's FinishLine and licensed under GNU AGPLv3. + * See the LICENSE file in the repository root folder for details. + */ + +import { Box, Typography } from '@mui/material'; +import { Rule } from 'shared'; +import WarningIcon from '@mui/icons-material/Warning'; +import NERModal from '../../../components/NERModal'; + +interface DeleteRuleModalProps { + open: boolean; + onHide: () => void; + onConfirm: () => void; + rule: Rule; + totalRulesToDelete: number; +} + +const DeleteRuleModal = ({ open, onHide, onConfirm, rule, totalRulesToDelete }: DeleteRuleModalProps) => { + const hasChildren = rule.subRuleIds.length > 0; + const titlePrefix = hasChildren ? 'Delete Rule Section:' : 'Delete Rule:'; + + const modalTitle = rule.ruleContent + ? `${titlePrefix} ${rule.ruleCode} - ${rule.ruleContent}` + : `${titlePrefix} ${rule.ruleCode}`; + + return ( + + + {modalTitle} + + + + + {totalRulesToDelete} {totalRulesToDelete === 1 ? 'rule' : 'rules'} will be deleted + + + + + ); +}; + +export default DeleteRuleModal; diff --git a/src/frontend/src/pages/RulesPage/components/RulesetDeleteModal.tsx b/src/frontend/src/pages/RulesPage/components/RulesetDeleteModal.tsx new file mode 100644 index 0000000000..911d4c8d00 --- /dev/null +++ b/src/frontend/src/pages/RulesPage/components/RulesetDeleteModal.tsx @@ -0,0 +1,27 @@ +import { Typography } from '@mui/material'; +import NERModal from '../../../components/NERModal'; + +interface RulesetDeleteModalProps { + rulesetName: string; + onDelete: () => void; + onHide: () => void; +} + +const RulesetDeleteModal: React.FC = ({ rulesetName, onDelete, onHide }) => { + return ( + + Are you sure you want to delete this ruleset? + {rulesetName} + + ); +}; + +export default RulesetDeleteModal; diff --git a/src/frontend/src/pages/RulesPage/components/RulesetGeneralView.tsx b/src/frontend/src/pages/RulesPage/components/RulesetGeneralView.tsx new file mode 100644 index 0000000000..56f556f019 --- /dev/null +++ b/src/frontend/src/pages/RulesPage/components/RulesetGeneralView.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Box, Paper, Table, TableBody, TableContainer } from '@mui/material'; +import { Rule } from 'shared'; +import RuleRow from '../RuleRow'; + +interface RulesetGeneralViewProps { + allRules: Rule[]; +} + +/** + * general view for displaying all top-level rules as dropdowns + */ +const RulesetGeneralView: React.FC = ({ allRules }) => { + const topLevelRules = allRules.filter((rule) => !rule.parentRule); + + return ( + + + + + {topLevelRules.map((rule) => ( + null} + backgroundColor="#9d9d9d" + textColor="#000000" + hoverColor="#5e5e5e" + rowHeight="10px" + verticalPadding="5px" + /> + ))} + +
+
+
+ ); +}; + +export default RulesetGeneralView; diff --git a/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx b/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx new file mode 100644 index 0000000000..003dbc408b --- /dev/null +++ b/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx @@ -0,0 +1,354 @@ +import React, { useState } from 'react'; +import { + Box, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + useMediaQuery, + useTheme, + Card, + CardContent, + Typography, + Stack, + Checkbox, + IconButton +} from '@mui/material'; +import { datePipe } from '../../../utils/pipes'; +import { NERButton } from '../../../components/NERButton'; +import { useHistory, useParams } from 'react-router-dom'; +import LoadingIndicator from '../../../components/LoadingIndicator'; +import ErrorPage from '../../ErrorPage'; +import { useDeleteRuleset, useRulesetsByType, useUpdateRuleset } from '../../../hooks/rules.hooks'; +import { Ruleset } from 'shared'; +import { routes } from '../../../utils/routes'; +import { useToast } from '../../../hooks/toasts.hooks'; +import { Delete } from '@mui/icons-material'; +import RulesetDeleteModal from './RulesetDeleteModal'; + +interface RulesetParams { + rulesetTypeId: string; +} + +interface RulesetDeleteButtonProps { + rulesetId: string; + name: string; + onDelete: (rulesetId: string, name: string) => void; +} + +const RulesetTable: React.FC = () => { + const { rulesetTypeId } = useParams(); + const toast = useToast(); + const history = useHistory(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + + const { data: rulesets = [], isLoading, error } = useRulesetsByType(rulesetTypeId); + const updateRuleset = useUpdateRuleset(); + const { mutateAsync: deleteRuleset } = useDeleteRuleset(); + + const hasRules = (ruleset: Ruleset) => { + return ruleset.ruleAmount > 0; + }; + + // Table header configuration + const headCells = [ + { id: 'fileName', label: 'File Name' }, + { id: 'dateUploaded', label: 'Date Uploaded' }, + { id: 'percentRulesAssigned', label: '% of Rules Assigned' }, + { id: 'car', label: 'Car' }, + { id: 'isActive', label: 'Active?' }, + { id: 'actions', label: 'Actions' }, + { id: 'delete', label: '' } + ]; + + const handleToggleActive = (ruleset: Ruleset) => { + updateRuleset.mutate( + { + rulesetId: ruleset.rulesetId, + name: ruleset.name, + isActive: !ruleset.active + }, + { + onSuccess: () => { + toast.success(ruleset.active ? 'Ruleset deactivated' : 'Ruleset activated'); + }, + onError: (error: any) => { + const message = error.response?.data?.message || error.message; + toast.error(message); + } + } + ); + }; + + const handleEditRuleset = (rulesetId: string) => { + history.push(routes.RULESET_EDIT.replace(':rulesetId', rulesetId)); + }; + + const handleViewRuleset = (rulesetId: string) => { + history.push(routes.RULESET_VIEW.replace(':rulesetId', rulesetId)); + }; + + const handleDeleteRuleset = async (rulesetId: string, name: string) => { + const ruleset = rulesets.find((r) => r.rulesetId === rulesetId); + + if (ruleset && ruleset.active) { + toast.error('Cannot delete an active ruleset. Please deactivate it first.'); + return; + } + + try { + await deleteRuleset(rulesetId); + toast.success(`Ruleset: ${name} deleted successfully!`); + } catch (error: unknown) { + if (error instanceof Error) { + toast.error(error.message); + } + } + }; + + const RulesetDeleteButton: React.FC = ({ rulesetId, name, onDelete }) => { + const [showDeleteModal, setShowDeleteModal] = useState(false); + + const handleDeleteSubmit = () => { + onDelete(rulesetId, name); + setShowDeleteModal(false); + }; + return ( + <> + setShowDeleteModal(true)}> + + + {showDeleteModal && ( + setShowDeleteModal(false)} /> + )} + + ); + }; + + if (isLoading) return ; + if (error) return ; + + return ( + + {isMobile ? ( + + {rulesets.map((ruleset: Ruleset) => ( + + + + {ruleset.name} + + + + + Date Uploaded: + + + {datePipe(ruleset.dateCreated)} + + + + + % of Rules Assigned: + + + {ruleset.assignedPercentage} + + + + + Car: + + + {ruleset.car.name} + + + + + Active: + + + {ruleset.active} + + handleToggleActive(ruleset)} + disabled={updateRuleset.isLoading} + sx={{ + color: '#fff', + '&.Mui-checked': { color: '#dd514c' } + }} + /> + + + handleEditRuleset(ruleset.rulesetId)} + disabled={!hasRules(ruleset)} + sx={{ + backgroundColor: theme.palette.grey[800], + color: theme.palette.getContrastText(theme.palette.grey[600]), + '&:hover': { + backgroundColor: theme.palette.grey[700] + }, + marginRight: '10px', + padding: '4px', + lineHeight: 1, + borderRadius: '6px', + '&.Mui-disabled': { + backgroundColor: theme.palette.grey[900], + color: theme.palette.grey[600] + } + }} + > + Edit/Assign Rules + + handleViewRuleset(ruleset.rulesetId)} + disabled={!hasRules(ruleset)} + sx={{ + backgroundColor: theme.palette.grey[800], + color: theme.palette.getContrastText(theme.palette.grey[600]), + '&:hover': { + backgroundColor: theme.palette.grey[700] + }, + padding: '4px', + lineHeight: 1, + borderRadius: '6px', + '&.Mui-disabled': { + backgroundColor: theme.palette.grey[900], + color: theme.palette.grey[600] + } + }} + > + View Rules + + + + + + + ))} + + ) : ( + + + + + {headCells.map((headCell) => ( + + {headCell.label} + + ))} + + + + {/* Table rows with ruleset data */} + {rulesets.length === 0 ? ( + + + No Rulesets Found + + + ) : ( + rulesets.map((ruleset: Ruleset) => ( + + + {ruleset.name} + + {datePipe(ruleset.dateCreated)} + {ruleset.assignedPercentage?.toFixed(2) ?? '0'}% + {ruleset.car.name} + + handleToggleActive(ruleset)} + disabled={updateRuleset.isLoading} + sx={{ + color: '#fff', + '&.Mui-checked': { color: '#dd514c' } + }} + /> + + + handleEditRuleset(ruleset.rulesetId)} + disabled={!hasRules(ruleset)} + sx={{ + backgroundColor: theme.palette.grey[800], + color: theme.palette.getContrastText(theme.palette.grey[600]), + '&:hover': { + backgroundColor: theme.palette.grey[700] + }, + marginRight: '10px', + padding: '4px', + lineHeight: 1, + borderRadius: '6px', + '&.Mui-disabled': { + backgroundColor: theme.palette.grey[900], + color: theme.palette.grey[600] + } + }} + > + Edit/Assign Rules + + handleViewRuleset(ruleset.rulesetId)} + disabled={!hasRules(ruleset)} + sx={{ + backgroundColor: theme.palette.grey[800], + color: theme.palette.getContrastText(theme.palette.grey[600]), + '&:hover': { + backgroundColor: theme.palette.grey[700] + }, + padding: '4px', + lineHeight: 1, + borderRadius: '6px', + '&.Mui-disabled': { + backgroundColor: theme.palette.grey[900], + color: theme.palette.grey[600] + } + }} + > + View Rules + + + + + + + )) + )} + +
+
+ )} +
+ ); +}; + +export default RulesetTable; diff --git a/src/frontend/src/pages/RulesPage/components/RulesetTeamView.tsx b/src/frontend/src/pages/RulesPage/components/RulesetTeamView.tsx new file mode 100644 index 0000000000..fe84ccc53e --- /dev/null +++ b/src/frontend/src/pages/RulesPage/components/RulesetTeamView.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import { Box, Paper, Table, TableBody, TableContainer } from '@mui/material'; +import { Rule } from 'shared'; +import RuleRow from '../RuleRow'; + +interface TeamProject { + projectId: string; + projectName: string; + rules: Rule[]; +} + +interface TeamRules { + teamId: string; + teamName: string; + projects: TeamProject[]; + unassignedRules: Rule[]; +} + +interface RulesetTeamViewProps { + allRules: Rule[]; + teamRules: TeamRules[]; + unassignedToTeam: Rule[]; +} + +/** + * Displays rules organized by team and project + * Teams and projects are rendered as RuleRows for consistent formatting + */ +const RulesetTeamView: React.FC = ({ allRules, teamRules, unassignedToTeam }) => { + // Convert teams to mock rules for rendering with RuleRow + const teamRulesAsRules: Rule[] = teamRules.map((team) => ({ + ruleId: `team-${team.teamId}`, + ruleCode: `${team.teamName}`, + ruleContent: '', + imageFileIds: [], + parentRule: undefined, + subRuleIds: [ + ...team.projects.map((p) => `project-${p.projectId}`), + ...(team.unassignedRules.length > 0 ? [`team-${team.teamId}-unassigned`] : []) + ], + referencedRuleIds: [] + })); + + // Convert projects to mock rules for rendering with RuleRow + const projectRulesAsRules: Rule[] = teamRules.flatMap((team) => + team.projects.map((project) => ({ + ruleId: `project-${project.projectId}`, + ruleCode: `${project.projectName}`, + ruleContent: '', + imageFileIds: [], + parentRule: { + ruleId: `team-${team.teamId}`, + ruleCode: `${team.teamName}` + }, + subRuleIds: project.rules.map((r) => r.ruleId), + referencedRuleIds: [] + })) + ); + + // Convert unassigned to project sections to mock rules + const unassignedToProjectRules: Rule[] = teamRules + .filter((team) => team.unassignedRules.length > 0) + .map((team) => ({ + ruleId: `team-${team.teamId}-unassigned`, + ruleCode: 'Unassigned Rules - Unassigned to Project', + ruleContent: '', + imageFileIds: [], + parentRule: { + ruleId: `team-${team.teamId}`, + ruleCode: `${team.teamName}` + }, + subRuleIds: team.unassignedRules.map((r) => r.ruleId), + referencedRuleIds: [] + })); + + // Create unassigned to team mock rule + const unassignedToTeamRule: Rule | null = + unassignedToTeam.length > 0 + ? { + ruleId: 'unassigned-to-team', + ruleCode: 'Unassigned Rules - Unassigned to Team', + ruleContent: '', + imageFileIds: [], + parentRule: undefined, + subRuleIds: unassignedToTeam.map((r) => r.ruleId), + referencedRuleIds: [] + } + : null; + + // mock team/project rules + actual rules + const allRulesIncludingMock = [ + ...teamRulesAsRules, + ...projectRulesAsRules, + ...unassignedToProjectRules, + ...(unassignedToTeamRule ? [unassignedToTeamRule] : []), + ...allRules + ]; + + // Top level items are teams and unassigned to team + const topLevelItems = [...teamRulesAsRules, ...(unassignedToTeamRule ? [unassignedToTeamRule] : [])]; + + return ( + + + + + {topLevelItems.map((item) => ( + null} + backgroundColor="#9d9d9d" + textColor="#000000" + hoverColor="#5e5e5e" + rowHeight="10px" + verticalPadding="5px" + initiallyExpanded={item.ruleId.startsWith('team-')} + /> + ))} + +
+
+
+ ); +}; + +export default RulesetTeamView; +export type { TeamProject, TeamRules }; diff --git a/src/frontend/src/pages/RulesPage/components/RulesetTypeDeleteModal.tsx b/src/frontend/src/pages/RulesPage/components/RulesetTypeDeleteModal.tsx new file mode 100644 index 0000000000..ad48f500d2 --- /dev/null +++ b/src/frontend/src/pages/RulesPage/components/RulesetTypeDeleteModal.tsx @@ -0,0 +1,27 @@ +import { Typography } from '@mui/material'; +import NERModal from '../../../components/NERModal'; + +interface RulesetTypeDeleteModalProps { + rulesetTypeName: string; + onDelete: () => void; + onHide: () => void; +} + +const RulesetTypeDeleteModal: React.FC = ({ rulesetTypeName, onDelete, onHide }) => { + return ( + + Are you sure you want to delete this ruleset type? + {rulesetTypeName} + + ); +}; + +export default RulesetTypeDeleteModal; diff --git a/src/frontend/src/pages/RulesPage/components/RulesetTypeTable.tsx b/src/frontend/src/pages/RulesPage/components/RulesetTypeTable.tsx new file mode 100644 index 0000000000..4ef2c1c2eb --- /dev/null +++ b/src/frontend/src/pages/RulesPage/components/RulesetTypeTable.tsx @@ -0,0 +1,259 @@ +import { + Box, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + useMediaQuery, + useTheme, + Card, + CardContent, + Typography, + Stack, + IconButton +} from '@mui/material'; +import { useHistory } from 'react-router-dom'; +import { datePipe } from '../../../utils/pipes'; +import { routes } from '../../../utils/routes'; +import { useAllRulesetTypes, useDeleteRulesetType } from '../../../hooks/rules.hooks'; +import LoadingIndicator from '../../../components/LoadingIndicator'; +import ErrorPage from '../../ErrorPage'; +import { RulesetType } from 'shared'; +import { NERButton } from '../../../components/NERButton'; +import { useToast } from '../../../hooks/toasts.hooks'; +import { useState } from 'react'; +import RulesetTypeDeleteModal from './RulesetTypeDeleteModal'; +import { Delete } from '@mui/icons-material'; + +type RulesetTypeColumnId = 'id' | 'name' | 'lastUpdated' | 'revisions' | 'actions' | 'delete'; + +interface RulesetTypeHeadCell { + id: RulesetTypeColumnId; + label: string; +} + +interface RulesetTypeDeleteButtonProps { + rulesetTypeId: string; + name: string; + onDelete: (rulesetTypeId: string, name: string) => void; +} + +const RulesetTypeTable: React.FC = () => { + const history = useHistory(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const toast = useToast(); + + const { data: rulesetTypes = [], isLoading, error } = useAllRulesetTypes(); + const { mutateAsync: deleteRulesetType } = useDeleteRulesetType(); + + const headCells: readonly RulesetTypeHeadCell[] = [ + { + id: 'name', + label: 'Ruleset Name' + }, + { + id: 'lastUpdated', + label: 'Last Updated' + }, + { + id: 'revisions', + label: 'Number of Revisions' + }, + { + id: 'actions', + label: 'Actions' + }, + { + id: 'delete', + label: '' + } + ]; + + const handleViewRulesetType = (rulesetTypeId: string) => { + history.push(routes.RULESET_BY_ID.replace(':rulesetTypeId', rulesetTypeId)); + }; + + const handleDeleteRulesetType = async (rulesetTypeId: string, name: string) => { + const rulesetType = rulesetTypes.find((rt) => rt.rulesetTypeId === rulesetTypeId); + if (rulesetType && rulesetType.revisionFiles.length > 0) { + toast.error('Cannot delete ruleset type with existing revisions'); + return; + } + + try { + await deleteRulesetType(rulesetTypeId); + toast.success(`Ruleset Type: ${name} deleted successfully!`); + } catch (error: unknown) { + if (error instanceof Error) { + toast.error(error.message); + } + } + }; + + const RulesetTypeDeleteButton: React.FC = ({ rulesetTypeId, name, onDelete }) => { + const [showDeleteModal, setShowDeleteModal] = useState(false); + + const handleDeleteSubmit = () => { + onDelete(rulesetTypeId, name); + setShowDeleteModal(false); + }; + + return ( + <> + setShowDeleteModal(true)}> + + + {showDeleteModal && ( + setShowDeleteModal(false)} + /> + )} + + ); + }; + + if (isLoading) return ; + if (error) return ; + + return ( + + {isMobile ? ( + + {rulesetTypes.map((rulesetType: RulesetType) => ( + + + + {rulesetType.name} + + + + + Last Updated: + + + {datePipe(rulesetType.lastUpdated)} + + + + + Revisions: + + + {rulesetType.revisionFiles.length} + + + + handleViewRulesetType(rulesetType.rulesetTypeId)} + > + View Rulesets + + + + + + + ))} + + ) : ( + + + + + {headCells.map((headCell) => ( + + {headCell.label} + + ))} + + + + {rulesetTypes.length === 0 ? ( + + + No Ruleset Types Found + + + ) : ( + rulesetTypes.map((rulesetType: RulesetType) => ( + + + {rulesetType.name} + + {datePipe(rulesetType.lastUpdated)} + {rulesetType.revisionFiles.length} + + handleViewRulesetType(rulesetType.rulesetTypeId)} + > + View Rulesets + + + + + + + )) + )} + +
+
+ )} +
+ ); +}; + +export default RulesetTypeTable; diff --git a/src/frontend/src/utils/routes.ts b/src/frontend/src/utils/routes.ts index f2317a1af0..72dcfcab3d 100644 --- a/src/frontend/src/utils/routes.ts +++ b/src/frontend/src/utils/routes.ts @@ -80,6 +80,12 @@ const GRAPH_COLLECTION_BY_ID = '/statistics/graph-collections/:graphCollectionId /**************** Retrospective ****************/ const RETROSPECTIVE = `/retrospective`; +/**************** Rules ****************/ +const RULES = `/rules`; +const RULESET_BY_ID = RULES + `/:rulesetTypeId`; +const RULESET_VIEW = RULES + `/ruleset/:rulesetId/view`; +const RULESET_EDIT = RULES + `/ruleset/:rulesetId/edit`; + export const routes = { BASE, LOGIN, @@ -145,5 +151,10 @@ export const routes = { EDIT_GRAPH, GRAPH_COLLECTION_BY_ID, - RETROSPECTIVE + RETROSPECTIVE, + + RULES, + RULESET_BY_ID, + RULESET_VIEW, + RULESET_EDIT }; diff --git a/src/frontend/src/utils/rules.utils.ts b/src/frontend/src/utils/rules.utils.ts new file mode 100644 index 0000000000..c0804c7158 --- /dev/null +++ b/src/frontend/src/utils/rules.utils.ts @@ -0,0 +1,22 @@ +/* + * This file is part of NER's FinishLine and licensed under GNU AGPLv3. + * See the LICENSE file in the repository root folder for details. + */ + +import { Rule } from 'shared'; + +/** + * Counts the total number of rules that will be deleted when deleting a rule + * (including the rule itself and all its descendants) + * @param rule - The rule to delete + * @param allRules - All rules in the ruleset + * @returns The total number of rules that will be deleted + */ +export const countRulesToDelete = (rule: Rule, allRules: Rule[]): number => { + let count = 1; + const children = allRules.filter((r) => rule.subRuleIds.includes(r.ruleId)); + for (const child of children) { + count += countRulesToDelete(child, allRules); + } + return count; +}; diff --git a/src/frontend/src/utils/urls.ts b/src/frontend/src/utils/urls.ts index f8367bc87f..23f27bcdd6 100644 --- a/src/frontend/src/utils/urls.ts +++ b/src/frontend/src/utils/urls.ts @@ -436,6 +436,37 @@ const retrospectiveTimelines = (startDate?: Date, endDate?: Date) => (endDate ? `end=${encodeURIComponent(endDate.toISOString())}` : ''); const retrospectiveBudgets = () => `${API_URL}/retrospective/budgets`; +/**************** Rules Endpoints ****************/ +const rules = () => `${API_URL}/rules`; +const rulesTopLevel = (rulesetId: string) => `${rules()}/${rulesetId}/parentRules`; +const rulesToggleTeam = (ruleId: string) => `${rules()}/rule/${ruleId}/toggle-team`; +const rulesChildRules = (ruleId: string) => `${rules()}/${ruleId}/subrules`; +const rulesTeamRulesInRulesetType = (rulesetTypeId: string, teamId: string) => `${rules()}/${rulesetTypeId}/team/${teamId}`; +const rulesetTypes = () => `${rules()}/rulesetTypes`; +const rulesetsByType = (rulesetTypeId: string) => `${rules()}/rulesets/${rulesetTypeId}`; +const ruleset = () => `${rules()}/ruleset`; +const rulesetTypeCreate = () => `${rules()}/rulesetType/create`; +const rulesetsCreate = () => `${ruleset()}/create`; +const rulesetById = (rulesetId: string) => `${ruleset()}/${rulesetId}`; +const ruleCreate = () => `${rules()}/rule/create`; +const parseRuleset = (rulesetId: string) => `${rulesetById(rulesetId)}/parse`; +const uploadRulesetFile = () => `${rules()}/upload/file`; +const rulesGetActiveRuleset = (rulesetTypeId: string) => `${rules()}/rulesetType/${rulesetTypeId}/active`; +const rulesGetProjectRules = (rulesetId: string, projectId: string) => + `${rules()}/ruleset/${rulesetId}/project/${projectId}/rules`; +const rulesGetUnassignedRulesForRuleset = (rulesetId: string, teamId: string) => + `${rules()}/ruleset/${rulesetId}/team/${teamId}/rules/unassigned`; +const rulesCreateProjectRule = () => `${rules()}/projectRule/create`; +const rulesDeleteProjectRule = (projectRuleId: string) => `${rules()}/projectRule/${projectRuleId}/delete`; +const rulesEditProjectRuleStatus = (projectRuleId: string) => `${rules()}/projectRule/${projectRuleId}/editStatus`; +const rulesEdit = (ruleId: string) => `${rules()}/rule/${ruleId}/edit`; +const rulesDelete = (ruleId: string) => `${rules()}/rule/${ruleId}/delete`; +const rulesetUpdate = (rulesetId: string) => `${ruleset()}/${rulesetId}/update`; +const rulesetDelete = (rulesetId: string) => `${ruleset()}/${rulesetId}/delete`; +const rulesetTypeDelete = (rulesetTypeId: string) => `${rules()}/rulesetType/${rulesetTypeId}/delete`; +const rulesetType = (rulesetTypeId: string) => `${rules()}/${rulesetTypeId}`; +const singleRuleset = (rulesetId: string) => `${rules()}/ruleset/${rulesetId}`; + /**************** Other Endpoints ****************/ const version = () => `https://api.github.com/repos/Northeastern-Electric-Racing/FinishLine/releases/latest`; @@ -738,5 +769,33 @@ export const apiUrls = { retrospectiveTimelines, retrospectiveBudgets, + rules, + rulesTopLevel, + rulesToggleTeam, + rulesChildRules, + rulesTeamRulesInRulesetType, + ruleset, + rulesetTypes, + rulesetsByType, + rulesetTypeCreate, + rulesetsCreate, + rulesetById, + ruleCreate, + rulesGetActiveRuleset, + rulesGetProjectRules, + rulesGetUnassignedRulesForRuleset, + rulesCreateProjectRule, + rulesDeleteProjectRule, + rulesEditProjectRuleStatus, + rulesEdit, + rulesDelete, + rulesetUpdate, + rulesetDelete, + rulesetTypeDelete, + rulesetType, + parseRuleset, + uploadRulesetFile, + singleRuleset, + version }; diff --git a/src/shared/index.ts b/src/shared/index.ts index 1b96e36456..09123e48fc 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -29,5 +29,6 @@ export * from './src/word-count'; export * from './src/permission-utils'; export * from './src/types/bom-types'; export * from './src/types/statistics-types'; +export * from './src/types/rules-types'; export * from './src/utils'; diff --git a/src/shared/src/types/rules-types.ts b/src/shared/src/types/rules-types.ts new file mode 100644 index 0000000000..6bbf9f289a --- /dev/null +++ b/src/shared/src/types/rules-types.ts @@ -0,0 +1,79 @@ +/* + * This file is part of NER's FinishLine and licensed under GNU AGPLv3. + * See the LICENSE file in the repository root folder for details. + */ + +import { User } from './user-types'; + +export enum RuleCompletion { + REVIEW = 'REVIEW', + INCOMPLETE = 'INCOMPLETE', + COMPLETED = 'COMPLETED' +} + +export interface RulesetType { + rulesetTypeId: string; + name: string; + lastUpdated: Date; + revisionFiles: Ruleset[]; +} + +export interface Ruleset { + rulesetId: string; + fileId: string; + name: string; + dateCreated: Date; + active: boolean; + rulesetType: RulesetType; + assignedPercentage: number; + car: { + carId: string; + name: string; + }; + ruleAmount: number; +} + +export interface Rule { + ruleId: string; + ruleCode: string; + ruleContent: string; + imageFileIds: string[]; + parentRule?: { + ruleId: string; + ruleCode: string; + }; + subRuleIds: string[]; + referencedRuleIds: string[]; + teams?: Array<{ + teamId: string; + teamName: string; + }>; +} + +export interface RuleStatusChange { + historyId: string; + projectRuleId: string; + createdBy: User; + dateCreated: Date; + newStatus: RuleCompletion; + note: string; +} + +export interface ProjectRule { + projectRuleId: string; + rule: Rule; + projectId: string; + currentStatus: RuleCompletion; + statusHistory: RuleStatusChange[]; +} + +export interface RulesetPreview { + name: string; + dateCreated: Date; + active: boolean; + assignedPercentage: number; + car: { + carId: string; + name: string; + }; +} diff --git a/yarn.lock b/yarn.lock index 6e5ea4207e..a6afc203d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -79,383 +79,399 @@ __metadata: linkType: hard "@aws-sdk/client-ses@npm:^3.731.1": - version: 3.911.0 - resolution: "@aws-sdk/client-ses@npm:3.911.0" + version: 3.936.0 + resolution: "@aws-sdk/client-ses@npm:3.936.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/core": 3.911.0 - "@aws-sdk/credential-provider-node": 3.911.0 - "@aws-sdk/middleware-host-header": 3.910.0 - "@aws-sdk/middleware-logger": 3.910.0 - "@aws-sdk/middleware-recursion-detection": 3.910.0 - "@aws-sdk/middleware-user-agent": 3.911.0 - "@aws-sdk/region-config-resolver": 3.910.0 - "@aws-sdk/types": 3.910.0 - "@aws-sdk/util-endpoints": 3.910.0 - "@aws-sdk/util-user-agent-browser": 3.910.0 - "@aws-sdk/util-user-agent-node": 3.911.0 - "@smithy/config-resolver": ^4.3.2 - "@smithy/core": ^3.16.1 - "@smithy/fetch-http-handler": ^5.3.3 - "@smithy/hash-node": ^4.2.2 - "@smithy/invalid-dependency": ^4.2.2 - "@smithy/middleware-content-length": ^4.2.2 - "@smithy/middleware-endpoint": ^4.3.3 - "@smithy/middleware-retry": ^4.4.3 - "@smithy/middleware-serde": ^4.2.2 - "@smithy/middleware-stack": ^4.2.2 - "@smithy/node-config-provider": ^4.3.2 - "@smithy/node-http-handler": ^4.4.1 - "@smithy/protocol-http": ^5.3.2 - "@smithy/smithy-client": ^4.8.1 - "@smithy/types": ^4.7.1 - "@smithy/url-parser": ^4.2.2 + "@aws-sdk/core": 3.936.0 + "@aws-sdk/credential-provider-node": 3.936.0 + "@aws-sdk/middleware-host-header": 3.936.0 + "@aws-sdk/middleware-logger": 3.936.0 + "@aws-sdk/middleware-recursion-detection": 3.936.0 + "@aws-sdk/middleware-user-agent": 3.936.0 + "@aws-sdk/region-config-resolver": 3.936.0 + "@aws-sdk/types": 3.936.0 + "@aws-sdk/util-endpoints": 3.936.0 + "@aws-sdk/util-user-agent-browser": 3.936.0 + "@aws-sdk/util-user-agent-node": 3.936.0 + "@smithy/config-resolver": ^4.4.3 + "@smithy/core": ^3.18.5 + "@smithy/fetch-http-handler": ^5.3.6 + "@smithy/hash-node": ^4.2.5 + "@smithy/invalid-dependency": ^4.2.5 + "@smithy/middleware-content-length": ^4.2.5 + "@smithy/middleware-endpoint": ^4.3.12 + "@smithy/middleware-retry": ^4.4.12 + "@smithy/middleware-serde": ^4.2.6 + "@smithy/middleware-stack": ^4.2.5 + "@smithy/node-config-provider": ^4.3.5 + "@smithy/node-http-handler": ^4.4.5 + "@smithy/protocol-http": ^5.3.5 + "@smithy/smithy-client": ^4.9.8 + "@smithy/types": ^4.9.0 + "@smithy/url-parser": ^4.2.5 "@smithy/util-base64": ^4.3.0 "@smithy/util-body-length-browser": ^4.2.0 "@smithy/util-body-length-node": ^4.2.1 - "@smithy/util-defaults-mode-browser": ^4.3.2 - "@smithy/util-defaults-mode-node": ^4.2.3 - "@smithy/util-endpoints": ^3.2.2 - "@smithy/util-middleware": ^4.2.2 - "@smithy/util-retry": ^4.2.2 + "@smithy/util-defaults-mode-browser": ^4.3.11 + "@smithy/util-defaults-mode-node": ^4.2.14 + "@smithy/util-endpoints": ^3.2.5 + "@smithy/util-middleware": ^4.2.5 + "@smithy/util-retry": ^4.2.5 "@smithy/util-utf8": ^4.2.0 - "@smithy/util-waiter": ^4.2.2 + "@smithy/util-waiter": ^4.2.5 tslib: ^2.6.2 - checksum: 039566263ecc9129d14d77db2d28a1deb4a532aa7680c34f257b89663d650ec406a3032797effcab55fd859aa3184389ef1f66495773db162f6dfc96bef1c9bf + checksum: e8e6d9c8728b05720eb3f152b7d25b74fe9151922cdc01fac63a5a5f0aff1e820e1f157073d8bd208a023fc11187e4cfec6d75d5ed5063b07f7faf8cc81e5759 languageName: node linkType: hard -"@aws-sdk/client-sso@npm:3.911.0": - version: 3.911.0 - resolution: "@aws-sdk/client-sso@npm:3.911.0" +"@aws-sdk/client-sso@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/client-sso@npm:3.936.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/core": 3.911.0 - "@aws-sdk/middleware-host-header": 3.910.0 - "@aws-sdk/middleware-logger": 3.910.0 - "@aws-sdk/middleware-recursion-detection": 3.910.0 - "@aws-sdk/middleware-user-agent": 3.911.0 - "@aws-sdk/region-config-resolver": 3.910.0 - "@aws-sdk/types": 3.910.0 - "@aws-sdk/util-endpoints": 3.910.0 - "@aws-sdk/util-user-agent-browser": 3.910.0 - "@aws-sdk/util-user-agent-node": 3.911.0 - "@smithy/config-resolver": ^4.3.2 - "@smithy/core": ^3.16.1 - "@smithy/fetch-http-handler": ^5.3.3 - "@smithy/hash-node": ^4.2.2 - "@smithy/invalid-dependency": ^4.2.2 - "@smithy/middleware-content-length": ^4.2.2 - "@smithy/middleware-endpoint": ^4.3.3 - "@smithy/middleware-retry": ^4.4.3 - "@smithy/middleware-serde": ^4.2.2 - "@smithy/middleware-stack": ^4.2.2 - "@smithy/node-config-provider": ^4.3.2 - "@smithy/node-http-handler": ^4.4.1 - "@smithy/protocol-http": ^5.3.2 - "@smithy/smithy-client": ^4.8.1 - "@smithy/types": ^4.7.1 - "@smithy/url-parser": ^4.2.2 + "@aws-sdk/core": 3.936.0 + "@aws-sdk/middleware-host-header": 3.936.0 + "@aws-sdk/middleware-logger": 3.936.0 + "@aws-sdk/middleware-recursion-detection": 3.936.0 + "@aws-sdk/middleware-user-agent": 3.936.0 + "@aws-sdk/region-config-resolver": 3.936.0 + "@aws-sdk/types": 3.936.0 + "@aws-sdk/util-endpoints": 3.936.0 + "@aws-sdk/util-user-agent-browser": 3.936.0 + "@aws-sdk/util-user-agent-node": 3.936.0 + "@smithy/config-resolver": ^4.4.3 + "@smithy/core": ^3.18.5 + "@smithy/fetch-http-handler": ^5.3.6 + "@smithy/hash-node": ^4.2.5 + "@smithy/invalid-dependency": ^4.2.5 + "@smithy/middleware-content-length": ^4.2.5 + "@smithy/middleware-endpoint": ^4.3.12 + "@smithy/middleware-retry": ^4.4.12 + "@smithy/middleware-serde": ^4.2.6 + "@smithy/middleware-stack": ^4.2.5 + "@smithy/node-config-provider": ^4.3.5 + "@smithy/node-http-handler": ^4.4.5 + "@smithy/protocol-http": ^5.3.5 + "@smithy/smithy-client": ^4.9.8 + "@smithy/types": ^4.9.0 + "@smithy/url-parser": ^4.2.5 "@smithy/util-base64": ^4.3.0 "@smithy/util-body-length-browser": ^4.2.0 "@smithy/util-body-length-node": ^4.2.1 - "@smithy/util-defaults-mode-browser": ^4.3.2 - "@smithy/util-defaults-mode-node": ^4.2.3 - "@smithy/util-endpoints": ^3.2.2 - "@smithy/util-middleware": ^4.2.2 - "@smithy/util-retry": ^4.2.2 + "@smithy/util-defaults-mode-browser": ^4.3.11 + "@smithy/util-defaults-mode-node": ^4.2.14 + "@smithy/util-endpoints": ^3.2.5 + "@smithy/util-middleware": ^4.2.5 + "@smithy/util-retry": ^4.2.5 "@smithy/util-utf8": ^4.2.0 tslib: ^2.6.2 - checksum: a2ba298bd345ff9c5b8be15000e487f73f4b174a63f3a93841e2e669df91eb91ae8ecee3e446d459d7b1560b47e9820ba020b2ea2a7dd8ecebecca50efcaf670 + checksum: c2915b723ac39d61b6989d0f7cee598a4c1d10d305834aa49f402b5076bb48d879ee6f297531c7b525bcc4e52b9a06777cb321a973a9742048e04122cb751981 languageName: node linkType: hard -"@aws-sdk/core@npm:3.911.0": - version: 3.911.0 - resolution: "@aws-sdk/core@npm:3.911.0" +"@aws-sdk/core@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/core@npm:3.936.0" dependencies: - "@aws-sdk/types": 3.910.0 - "@aws-sdk/xml-builder": 3.911.0 - "@smithy/core": ^3.16.1 - "@smithy/node-config-provider": ^4.3.2 - "@smithy/property-provider": ^4.2.2 - "@smithy/protocol-http": ^5.3.2 - "@smithy/signature-v4": ^5.3.2 - "@smithy/smithy-client": ^4.8.1 - "@smithy/types": ^4.7.1 + "@aws-sdk/types": 3.936.0 + "@aws-sdk/xml-builder": 3.930.0 + "@smithy/core": ^3.18.5 + "@smithy/node-config-provider": ^4.3.5 + "@smithy/property-provider": ^4.2.5 + "@smithy/protocol-http": ^5.3.5 + "@smithy/signature-v4": ^5.3.5 + "@smithy/smithy-client": ^4.9.8 + "@smithy/types": ^4.9.0 "@smithy/util-base64": ^4.3.0 - "@smithy/util-middleware": ^4.2.2 + "@smithy/util-middleware": ^4.2.5 "@smithy/util-utf8": ^4.2.0 tslib: ^2.6.2 - checksum: c4a4bf5b2611c22bcd7ddde22c4c49d0390c98ac834ce017063902040ba009a4356975ec47bef0a951219778059e58a8ac418b30e20a8435b3b91faf6b2d12e2 + checksum: a5b1b08f6955d81254d149068de13d3859d56d4dbcddee5d420432d9b12ac4a2ee7cb05082fdac0a4377f07e6bed4c38dc37abcc688018144adedf97869fa190 languageName: node linkType: hard -"@aws-sdk/credential-provider-env@npm:3.911.0": - version: 3.911.0 - resolution: "@aws-sdk/credential-provider-env@npm:3.911.0" +"@aws-sdk/credential-provider-env@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/credential-provider-env@npm:3.936.0" dependencies: - "@aws-sdk/core": 3.911.0 - "@aws-sdk/types": 3.910.0 - "@smithy/property-provider": ^4.2.2 - "@smithy/types": ^4.7.1 + "@aws-sdk/core": 3.936.0 + "@aws-sdk/types": 3.936.0 + "@smithy/property-provider": ^4.2.5 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: f36af9de54f8ed961100d5e8fd91414011c847144d6cf7075bc66b7704162fcc1e89485e31a6d86bf7c1eab08c49add68d67b893ccb7d378e55219a985ad0884 + checksum: 7388fe3f59e8a47a94e4e2aa63827e118a2dbb797c75e4bdc8b06631ce0248b9d366d4e728063cbd0a77aee5bd6d479d918d2cb9ddd696a4e9fc4a25377a5ad5 languageName: node linkType: hard -"@aws-sdk/credential-provider-http@npm:3.911.0": - version: 3.911.0 - resolution: "@aws-sdk/credential-provider-http@npm:3.911.0" +"@aws-sdk/credential-provider-http@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/credential-provider-http@npm:3.936.0" dependencies: - "@aws-sdk/core": 3.911.0 - "@aws-sdk/types": 3.910.0 - "@smithy/fetch-http-handler": ^5.3.3 - "@smithy/node-http-handler": ^4.4.1 - "@smithy/property-provider": ^4.2.2 - "@smithy/protocol-http": ^5.3.2 - "@smithy/smithy-client": ^4.8.1 - "@smithy/types": ^4.7.1 - "@smithy/util-stream": ^4.5.2 + "@aws-sdk/core": 3.936.0 + "@aws-sdk/types": 3.936.0 + "@smithy/fetch-http-handler": ^5.3.6 + "@smithy/node-http-handler": ^4.4.5 + "@smithy/property-provider": ^4.2.5 + "@smithy/protocol-http": ^5.3.5 + "@smithy/smithy-client": ^4.9.8 + "@smithy/types": ^4.9.0 + "@smithy/util-stream": ^4.5.6 tslib: ^2.6.2 - checksum: 547307bd6cc036315fd0f7416f366717b4269113751a4e960daef58ad73ea7c27cb47857a72d6f997273b2840cc53021b7eff6d390265fe0842090877c653538 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-ini@npm:3.911.0": - version: 3.911.0 - resolution: "@aws-sdk/credential-provider-ini@npm:3.911.0" - dependencies: - "@aws-sdk/core": 3.911.0 - "@aws-sdk/credential-provider-env": 3.911.0 - "@aws-sdk/credential-provider-http": 3.911.0 - "@aws-sdk/credential-provider-process": 3.911.0 - "@aws-sdk/credential-provider-sso": 3.911.0 - "@aws-sdk/credential-provider-web-identity": 3.911.0 - "@aws-sdk/nested-clients": 3.911.0 - "@aws-sdk/types": 3.910.0 - "@smithy/credential-provider-imds": ^4.2.2 - "@smithy/property-provider": ^4.2.2 - "@smithy/shared-ini-file-loader": ^4.3.2 - "@smithy/types": ^4.7.1 + checksum: 1a09a1038be861fbb05b5ca3c48185b090df3b54fbea0f45ee2c251cbe463f56243c9a52d14fd4bb217669ea22f6160b6cd58ab2225f58f479ff1b8f5629d7de + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-ini@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.936.0" + dependencies: + "@aws-sdk/core": 3.936.0 + "@aws-sdk/credential-provider-env": 3.936.0 + "@aws-sdk/credential-provider-http": 3.936.0 + "@aws-sdk/credential-provider-login": 3.936.0 + "@aws-sdk/credential-provider-process": 3.936.0 + "@aws-sdk/credential-provider-sso": 3.936.0 + "@aws-sdk/credential-provider-web-identity": 3.936.0 + "@aws-sdk/nested-clients": 3.936.0 + "@aws-sdk/types": 3.936.0 + "@smithy/credential-provider-imds": ^4.2.5 + "@smithy/property-provider": ^4.2.5 + "@smithy/shared-ini-file-loader": ^4.4.0 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: d675d215f3186663c55be5d1a049ad64c63d4fafcfe61bcf8a311b83e1772d9bd43e9b7408b62549c89d72a2f0d0e0485ae928d4d87cf76241d3add44f3a423d + checksum: 318761af10ac498365d5b0cfc06e3044a010591587e9f83f3e66055704bfc55455ffe2c300c824ef95cc133855cf73e5c86e97ee37745d8e9a6f8982d3113ff0 languageName: node linkType: hard -"@aws-sdk/credential-provider-node@npm:3.911.0": - version: 3.911.0 - resolution: "@aws-sdk/credential-provider-node@npm:3.911.0" +"@aws-sdk/credential-provider-login@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/credential-provider-login@npm:3.936.0" dependencies: - "@aws-sdk/credential-provider-env": 3.911.0 - "@aws-sdk/credential-provider-http": 3.911.0 - "@aws-sdk/credential-provider-ini": 3.911.0 - "@aws-sdk/credential-provider-process": 3.911.0 - "@aws-sdk/credential-provider-sso": 3.911.0 - "@aws-sdk/credential-provider-web-identity": 3.911.0 - "@aws-sdk/types": 3.910.0 - "@smithy/credential-provider-imds": ^4.2.2 - "@smithy/property-provider": ^4.2.2 - "@smithy/shared-ini-file-loader": ^4.3.2 - "@smithy/types": ^4.7.1 + "@aws-sdk/core": 3.936.0 + "@aws-sdk/nested-clients": 3.936.0 + "@aws-sdk/types": 3.936.0 + "@smithy/property-provider": ^4.2.5 + "@smithy/protocol-http": ^5.3.5 + "@smithy/shared-ini-file-loader": ^4.4.0 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: c767e7c29853591ba35da622bb63bd3c5e206cde2082c9efb8d86e29d8068eade9e659b0e9020241550f1b893d54883c98085281a6ed769925d73a787c599759 + checksum: 809fd57a96ed01bdaf1b585dd36f4e8ada75f2e24b7040af3ecc67d0ff8192170c9b44d0720453403531b9e5b7886ac15c9f241c7798396cdc2b33ac59694e1a languageName: node linkType: hard -"@aws-sdk/credential-provider-process@npm:3.911.0": - version: 3.911.0 - resolution: "@aws-sdk/credential-provider-process@npm:3.911.0" +"@aws-sdk/credential-provider-node@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.936.0" dependencies: - "@aws-sdk/core": 3.911.0 - "@aws-sdk/types": 3.910.0 - "@smithy/property-provider": ^4.2.2 - "@smithy/shared-ini-file-loader": ^4.3.2 - "@smithy/types": ^4.7.1 + "@aws-sdk/credential-provider-env": 3.936.0 + "@aws-sdk/credential-provider-http": 3.936.0 + "@aws-sdk/credential-provider-ini": 3.936.0 + "@aws-sdk/credential-provider-process": 3.936.0 + "@aws-sdk/credential-provider-sso": 3.936.0 + "@aws-sdk/credential-provider-web-identity": 3.936.0 + "@aws-sdk/types": 3.936.0 + "@smithy/credential-provider-imds": ^4.2.5 + "@smithy/property-provider": ^4.2.5 + "@smithy/shared-ini-file-loader": ^4.4.0 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: ef0c471ec1719178cfc2c8bd93795d88f42ff9e85a284c5f0f3f33d042a6ca584bd1efd9d711126da774505e49b9e586850b3def3513b23e4eb2384c29280ba3 + checksum: 999b52e5a8b4726e4f811182a303d6b5f3ea3797983449d1026185847819aedee5da6008ec28840e3b13aa2839e12749587f9e8ba1df7dbf740f02e69611dbc3 languageName: node linkType: hard -"@aws-sdk/credential-provider-sso@npm:3.911.0": - version: 3.911.0 - resolution: "@aws-sdk/credential-provider-sso@npm:3.911.0" +"@aws-sdk/credential-provider-process@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/credential-provider-process@npm:3.936.0" dependencies: - "@aws-sdk/client-sso": 3.911.0 - "@aws-sdk/core": 3.911.0 - "@aws-sdk/token-providers": 3.911.0 - "@aws-sdk/types": 3.910.0 - "@smithy/property-provider": ^4.2.2 - "@smithy/shared-ini-file-loader": ^4.3.2 - "@smithy/types": ^4.7.1 + "@aws-sdk/core": 3.936.0 + "@aws-sdk/types": 3.936.0 + "@smithy/property-provider": ^4.2.5 + "@smithy/shared-ini-file-loader": ^4.4.0 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 7820b20afe9ba13c3012e47f45f2ea91d84c62a430f3f941000c07c44c9b4b15990efe2c18f7007d7b2f0f1e29998ed27a2488528b4224b4655ba05aaf281912 + checksum: d7d32219512ae31d8c23a26fb7993b32715df8f7d5b2794db99b18a52a92d44ba121598299c328af51617b7be28fb264230482fedc33f704a6586f365db20dd4 languageName: node linkType: hard -"@aws-sdk/credential-provider-web-identity@npm:3.911.0": - version: 3.911.0 - resolution: "@aws-sdk/credential-provider-web-identity@npm:3.911.0" +"@aws-sdk/credential-provider-sso@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.936.0" dependencies: - "@aws-sdk/core": 3.911.0 - "@aws-sdk/nested-clients": 3.911.0 - "@aws-sdk/types": 3.910.0 - "@smithy/property-provider": ^4.2.2 - "@smithy/shared-ini-file-loader": ^4.3.2 - "@smithy/types": ^4.7.1 + "@aws-sdk/client-sso": 3.936.0 + "@aws-sdk/core": 3.936.0 + "@aws-sdk/token-providers": 3.936.0 + "@aws-sdk/types": 3.936.0 + "@smithy/property-provider": ^4.2.5 + "@smithy/shared-ini-file-loader": ^4.4.0 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: c09239ad3b55347258b128af9545003adef49e3b96440b22dfff76d00867af14a6a08bb04f4b1964ccfde3fc1d98f24aee12b4c782a84712765c07c5bc1aad87 + checksum: 2be3b5fcf6ed35d3228a1e92a12bb55771b74f46839903045dc9744e6c683175f5d48986b5f915675448d9513be761c1065af64a6168a8e9469ee74a4096d002 languageName: node linkType: hard -"@aws-sdk/middleware-host-header@npm:3.910.0": - version: 3.910.0 - resolution: "@aws-sdk/middleware-host-header@npm:3.910.0" +"@aws-sdk/credential-provider-web-identity@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.936.0" dependencies: - "@aws-sdk/types": 3.910.0 - "@smithy/protocol-http": ^5.3.2 - "@smithy/types": ^4.7.1 + "@aws-sdk/core": 3.936.0 + "@aws-sdk/nested-clients": 3.936.0 + "@aws-sdk/types": 3.936.0 + "@smithy/property-provider": ^4.2.5 + "@smithy/shared-ini-file-loader": ^4.4.0 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 08ea8343b18d0eb843d047cd1ff1ed06288022ec8e407c5f757c3cf4e4859137369d7cf1c55d259d023c1558e35f476b5d1260ce6a201884905cb0ea6d33ef23 + checksum: 445dfde91430c5797621897631cd6c4ddcf3c1961c92d2bb0b498ce8f0b3e85e1a91cc82b1d0e63665103c6257336b4bfa0584150f98eb1fb5c98b90b92d1292 languageName: node linkType: hard -"@aws-sdk/middleware-logger@npm:3.910.0": - version: 3.910.0 - resolution: "@aws-sdk/middleware-logger@npm:3.910.0" +"@aws-sdk/middleware-host-header@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/middleware-host-header@npm:3.936.0" dependencies: - "@aws-sdk/types": 3.910.0 - "@smithy/types": ^4.7.1 + "@aws-sdk/types": 3.936.0 + "@smithy/protocol-http": ^5.3.5 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 6fb0262af5222d515bbce824ba1ce80f976ca14312a431a60b551e85cb6b9711104c0f7695b5703ca6bc5127cbb49b10922ce99915635025cf26a88ed96ac0b3 + checksum: edd13115342ac016f70125f0337d28e764a9b513466e75af76a5301791a9d4842c02615254e430a00f6e9400a75d11113f1845f6d9d0eb6172012750795ab107 languageName: node linkType: hard -"@aws-sdk/middleware-recursion-detection@npm:3.910.0": - version: 3.910.0 - resolution: "@aws-sdk/middleware-recursion-detection@npm:3.910.0" +"@aws-sdk/middleware-logger@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/middleware-logger@npm:3.936.0" dependencies: - "@aws-sdk/types": 3.910.0 - "@aws/lambda-invoke-store": ^0.0.1 - "@smithy/protocol-http": ^5.3.2 - "@smithy/types": ^4.7.1 + "@aws-sdk/types": 3.936.0 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: adfd61a4561ca4ab3f2d673a2a5c04be1c39970b13530535082e91046615cbeff433c0340f5f509423a0af10eb63ee82b5072dc2f8dddbe62070b099ea4894da + checksum: b25fcb51b749648e6979760838e18a683392d4505879ecc99621ec6e76317f3ef772c46b40b8e0a3c7a587151bcfcb1d10bce0ae23814483231cac3e462269c0 languageName: node linkType: hard -"@aws-sdk/middleware-user-agent@npm:3.911.0": - version: 3.911.0 - resolution: "@aws-sdk/middleware-user-agent@npm:3.911.0" +"@aws-sdk/middleware-recursion-detection@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.936.0" dependencies: - "@aws-sdk/core": 3.911.0 - "@aws-sdk/types": 3.910.0 - "@aws-sdk/util-endpoints": 3.910.0 - "@smithy/core": ^3.16.1 - "@smithy/protocol-http": ^5.3.2 - "@smithy/types": ^4.7.1 + "@aws-sdk/types": 3.936.0 + "@aws/lambda-invoke-store": ^0.2.0 + "@smithy/protocol-http": ^5.3.5 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 2cc14409738f0f7e5eaec8238e775e4930a8a1227b16e87ce3eae2b57f3dc7f9d102016224df2485abdf98065c354cd6a0c717393c30293acdcbc20098b6e7f5 + checksum: 3da5e89afec0daf7bcc9d9062b8d5bdf7d241ac783b9a335d38152f30b06c1907f7aa9f7303ac3557aed38c63c425feaa3fde53690ab44c90e1ed693bf3c8549 languageName: node linkType: hard -"@aws-sdk/nested-clients@npm:3.911.0": - version: 3.911.0 - resolution: "@aws-sdk/nested-clients@npm:3.911.0" +"@aws-sdk/middleware-user-agent@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/middleware-user-agent@npm:3.936.0" + dependencies: + "@aws-sdk/core": 3.936.0 + "@aws-sdk/types": 3.936.0 + "@aws-sdk/util-endpoints": 3.936.0 + "@smithy/core": ^3.18.5 + "@smithy/protocol-http": ^5.3.5 + "@smithy/types": ^4.9.0 + tslib: ^2.6.2 + checksum: 850fce494ba53f06bca2be7cfdbe04ab5df68818ce5436969231f8dc9e2297c56eafd4003f86e33c8dcb22147708c97d9375459f0f0763dd94a1fe703179cd41 + languageName: node + linkType: hard + +"@aws-sdk/nested-clients@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/nested-clients@npm:3.936.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/core": 3.911.0 - "@aws-sdk/middleware-host-header": 3.910.0 - "@aws-sdk/middleware-logger": 3.910.0 - "@aws-sdk/middleware-recursion-detection": 3.910.0 - "@aws-sdk/middleware-user-agent": 3.911.0 - "@aws-sdk/region-config-resolver": 3.910.0 - "@aws-sdk/types": 3.910.0 - "@aws-sdk/util-endpoints": 3.910.0 - "@aws-sdk/util-user-agent-browser": 3.910.0 - "@aws-sdk/util-user-agent-node": 3.911.0 - "@smithy/config-resolver": ^4.3.2 - "@smithy/core": ^3.16.1 - "@smithy/fetch-http-handler": ^5.3.3 - "@smithy/hash-node": ^4.2.2 - "@smithy/invalid-dependency": ^4.2.2 - "@smithy/middleware-content-length": ^4.2.2 - "@smithy/middleware-endpoint": ^4.3.3 - "@smithy/middleware-retry": ^4.4.3 - "@smithy/middleware-serde": ^4.2.2 - "@smithy/middleware-stack": ^4.2.2 - "@smithy/node-config-provider": ^4.3.2 - "@smithy/node-http-handler": ^4.4.1 - "@smithy/protocol-http": ^5.3.2 - "@smithy/smithy-client": ^4.8.1 - "@smithy/types": ^4.7.1 - "@smithy/url-parser": ^4.2.2 + "@aws-sdk/core": 3.936.0 + "@aws-sdk/middleware-host-header": 3.936.0 + "@aws-sdk/middleware-logger": 3.936.0 + "@aws-sdk/middleware-recursion-detection": 3.936.0 + "@aws-sdk/middleware-user-agent": 3.936.0 + "@aws-sdk/region-config-resolver": 3.936.0 + "@aws-sdk/types": 3.936.0 + "@aws-sdk/util-endpoints": 3.936.0 + "@aws-sdk/util-user-agent-browser": 3.936.0 + "@aws-sdk/util-user-agent-node": 3.936.0 + "@smithy/config-resolver": ^4.4.3 + "@smithy/core": ^3.18.5 + "@smithy/fetch-http-handler": ^5.3.6 + "@smithy/hash-node": ^4.2.5 + "@smithy/invalid-dependency": ^4.2.5 + "@smithy/middleware-content-length": ^4.2.5 + "@smithy/middleware-endpoint": ^4.3.12 + "@smithy/middleware-retry": ^4.4.12 + "@smithy/middleware-serde": ^4.2.6 + "@smithy/middleware-stack": ^4.2.5 + "@smithy/node-config-provider": ^4.3.5 + "@smithy/node-http-handler": ^4.4.5 + "@smithy/protocol-http": ^5.3.5 + "@smithy/smithy-client": ^4.9.8 + "@smithy/types": ^4.9.0 + "@smithy/url-parser": ^4.2.5 "@smithy/util-base64": ^4.3.0 "@smithy/util-body-length-browser": ^4.2.0 "@smithy/util-body-length-node": ^4.2.1 - "@smithy/util-defaults-mode-browser": ^4.3.2 - "@smithy/util-defaults-mode-node": ^4.2.3 - "@smithy/util-endpoints": ^3.2.2 - "@smithy/util-middleware": ^4.2.2 - "@smithy/util-retry": ^4.2.2 + "@smithy/util-defaults-mode-browser": ^4.3.11 + "@smithy/util-defaults-mode-node": ^4.2.14 + "@smithy/util-endpoints": ^3.2.5 + "@smithy/util-middleware": ^4.2.5 + "@smithy/util-retry": ^4.2.5 "@smithy/util-utf8": ^4.2.0 tslib: ^2.6.2 - checksum: de682ee74b24451761aed70b2b9e02624852e42ee589ea1ba818d51195251feef44cf359a058499dc149824ae6df1de074f370d5a5a345b9b4f9c3eea91e1156 + checksum: f1935c96e6d1154803a3d8128d8e2f1dd8784745359463867cd12dcdb005a04a44ff020e4fe32067fcafcbdfdbe52baf775f6bc75610d46f6a229b63c3ee0b5a languageName: node linkType: hard -"@aws-sdk/region-config-resolver@npm:3.910.0": - version: 3.910.0 - resolution: "@aws-sdk/region-config-resolver@npm:3.910.0" +"@aws-sdk/region-config-resolver@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/region-config-resolver@npm:3.936.0" dependencies: - "@aws-sdk/types": 3.910.0 - "@smithy/node-config-provider": ^4.3.2 - "@smithy/types": ^4.7.1 - "@smithy/util-config-provider": ^4.2.0 - "@smithy/util-middleware": ^4.2.2 + "@aws-sdk/types": 3.936.0 + "@smithy/config-resolver": ^4.4.3 + "@smithy/node-config-provider": ^4.3.5 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 9eb747b70e0d331a20a663e755f71584236d398bc50c36f68d04b308d2d6947cc334d1efdc20d83beff4731acb5eb99501e27ee1920779f39b9c6b6b84be8dad + checksum: 19dcb7c3eb3c93a59ccbb5129716a48d33adbfa8999fd7494c7d8e91ab69914959d803349fd9409a8592f9614beb81e3ed6e1bd300db28cfd9eefee224cf75fb languageName: node linkType: hard -"@aws-sdk/token-providers@npm:3.911.0": - version: 3.911.0 - resolution: "@aws-sdk/token-providers@npm:3.911.0" +"@aws-sdk/token-providers@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/token-providers@npm:3.936.0" dependencies: - "@aws-sdk/core": 3.911.0 - "@aws-sdk/nested-clients": 3.911.0 - "@aws-sdk/types": 3.910.0 - "@smithy/property-provider": ^4.2.2 - "@smithy/shared-ini-file-loader": ^4.3.2 - "@smithy/types": ^4.7.1 + "@aws-sdk/core": 3.936.0 + "@aws-sdk/nested-clients": 3.936.0 + "@aws-sdk/types": 3.936.0 + "@smithy/property-provider": ^4.2.5 + "@smithy/shared-ini-file-loader": ^4.4.0 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 88af693059074dab550948eed38c5479d6e5ac0f66ebd179578e3ce1a530934becb67c9add7ab0a73174fccbbeaa7320afa92a59fd26f95bc996079052d4a6bd + checksum: 7c2728fb94b2a5470e211bff2e72406580a9c12fd62985d413b98bbc3b19974fcf690e45b0c4b11efb11df1ca8ed7016e63a14bd8065d97d87036c11c4043679 languageName: node linkType: hard -"@aws-sdk/types@npm:3.910.0, @aws-sdk/types@npm:^3.222.0": - version: 3.910.0 - resolution: "@aws-sdk/types@npm:3.910.0" +"@aws-sdk/types@npm:3.936.0, @aws-sdk/types@npm:^3.222.0": + version: 3.936.0 + resolution: "@aws-sdk/types@npm:3.936.0" dependencies: - "@smithy/types": ^4.7.1 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 81dc4370f86c8d65417958d1623efe31b0bacb6000118d56cee2faac02ec92289600f12b273219813fd2101f51d1b22ad013f8f9e72eaa16bc7a5866cdbc37bc + checksum: 352e767671d3dd5100e0b14dec344ac7f38c4d33fe3953c8b0a17cb5ba6e9bd42701fd68ac068cfab11853f62fc4ffb543f68c2f90db0a2d8ec6ad99d939a2b4 languageName: node linkType: hard -"@aws-sdk/util-endpoints@npm:3.910.0": - version: 3.910.0 - resolution: "@aws-sdk/util-endpoints@npm:3.910.0" +"@aws-sdk/util-endpoints@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/util-endpoints@npm:3.936.0" dependencies: - "@aws-sdk/types": 3.910.0 - "@smithy/types": ^4.7.1 - "@smithy/url-parser": ^4.2.2 - "@smithy/util-endpoints": ^3.2.2 + "@aws-sdk/types": 3.936.0 + "@smithy/types": ^4.9.0 + "@smithy/url-parser": ^4.2.5 + "@smithy/util-endpoints": ^3.2.5 tslib: ^2.6.2 - checksum: c36f552e038c8f08369dc8c16cdfcecc8911c2766c40ce0c63feaddd3c211352ca7bf0c178bcee108febaa42a72764f676b9b3a7ac9d12b6a37fe6510c678a9c + checksum: 12a9596501f573b037af3b4db68f95dd57d980f2f07e5030749e1460d1f2fe8a3990160aed0cde343e9e7a30acc7ac05efc70b9902f51aee5c47d4192c957414 languageName: node linkType: hard @@ -468,51 +484,51 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-user-agent-browser@npm:3.910.0": - version: 3.910.0 - resolution: "@aws-sdk/util-user-agent-browser@npm:3.910.0" +"@aws-sdk/util-user-agent-browser@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.936.0" dependencies: - "@aws-sdk/types": 3.910.0 - "@smithy/types": ^4.7.1 + "@aws-sdk/types": 3.936.0 + "@smithy/types": ^4.9.0 bowser: ^2.11.0 tslib: ^2.6.2 - checksum: 6313208d741d47a8ec946072a0b2d3a8346e4187a75e54f60974755c731dc6180a16e41452cbe8eced6d91005c336be2ee7131da2a5660dc0a78f0c63bb7ccb9 + checksum: 6ce8fe3059f0f3970cb24c227253c012c4633de8b15cb2a7e22b2df7434ef655f9305358fc3872c9f8cb731d42932285f614b499f36a74d9d866736ab5e39207 languageName: node linkType: hard -"@aws-sdk/util-user-agent-node@npm:3.911.0": - version: 3.911.0 - resolution: "@aws-sdk/util-user-agent-node@npm:3.911.0" +"@aws-sdk/util-user-agent-node@npm:3.936.0": + version: 3.936.0 + resolution: "@aws-sdk/util-user-agent-node@npm:3.936.0" dependencies: - "@aws-sdk/middleware-user-agent": 3.911.0 - "@aws-sdk/types": 3.910.0 - "@smithy/node-config-provider": ^4.3.2 - "@smithy/types": ^4.7.1 + "@aws-sdk/middleware-user-agent": 3.936.0 + "@aws-sdk/types": 3.936.0 + "@smithy/node-config-provider": ^4.3.5 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 peerDependencies: aws-crt: ">=1.0.0" peerDependenciesMeta: aws-crt: optional: true - checksum: b4989136f4c43489e56f17ada16e403642f9096e7f419d01251b717f2e6e92c20fe9629f33ae23074cca5f70c1f6f1daece301334268b56598ee25fca9b3629b + checksum: 6e2e5c4b9079621af1f02b0cc208fc607b17a036de31354642ca259bdc9bf805520966561be7d2f8c0c2de3c73bec508f20b0f5bc7bd3eaa05218b004eea0be9 languageName: node linkType: hard -"@aws-sdk/xml-builder@npm:3.911.0": - version: 3.911.0 - resolution: "@aws-sdk/xml-builder@npm:3.911.0" +"@aws-sdk/xml-builder@npm:3.930.0": + version: 3.930.0 + resolution: "@aws-sdk/xml-builder@npm:3.930.0" dependencies: - "@smithy/types": ^4.7.1 + "@smithy/types": ^4.9.0 fast-xml-parser: 5.2.5 tslib: ^2.6.2 - checksum: d245e7b2563676653e43eaae1cf0c2c85526e26f7a2be94bbac37b544c276cccc51e9b8f19f15c086d8292c76efe481fff55b33087d355addf77a534caa907b8 + checksum: 3e57e8e3be4706667d478f7198beaf0c0e49117312211c3e5640d5a1cee729b743fa957560d3814c1bc779698e203b93e5e4fb3c9a2accf58de38e7100fffd58 languageName: node linkType: hard -"@aws/lambda-invoke-store@npm:^0.0.1": - version: 0.0.1 - resolution: "@aws/lambda-invoke-store@npm:0.0.1" - checksum: af732ba2cd343daa49d4933827b4bdc80449641fbdf465ad4a97a818adf6f355454942a2b59a6a297c261c1b3fff11ea69c93b9564ed5e33fcdcf30f993c722d +"@aws/lambda-invoke-store@npm:^0.2.0": + version: 0.2.1 + resolution: "@aws/lambda-invoke-store@npm:0.2.1" + checksum: d688756a7f81c21060cde5658e689c2e9c79222640173df8b99a4750e6d34adf8009461f4145e8c5b1384d456f8a600bbd4bcfec58aae2d7af11150f63adb4a5 languageName: node linkType: hard @@ -536,39 +552,39 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.27.2, @babel/compat-data@npm:^7.27.7, @babel/compat-data@npm:^7.28.0": - version: 7.28.4 - resolution: "@babel/compat-data@npm:7.28.4" - checksum: 9f6f5289bbe5a29e3f9c737577a797205a91f19371b50af8942257d9cb590d44eb950154e4f2a3d5de4105f97a49d6fbc8daebe0db1e6eee04f5a4bf73536bfc +"@babel/compat-data@npm:^7.27.2, @babel/compat-data@npm:^7.27.7, @babel/compat-data@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/compat-data@npm:7.28.5" + checksum: d7bcb3ee713752dc27b89800bfb39f9ac5f3edc46b4f5bb9906e1fe6b6110c7b245dd502602ea66f93790480c228605e9a601f27c07016f24b56772e97bedbdf languageName: node linkType: hard "@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.1, @babel/core@npm:^7.12.3, @babel/core@npm:^7.16.0, @babel/core@npm:^7.20.5, @babel/core@npm:^7.28.0, @babel/core@npm:^7.7.2, @babel/core@npm:^7.8.0": - version: 7.28.4 - resolution: "@babel/core@npm:7.28.4" + version: 7.28.5 + resolution: "@babel/core@npm:7.28.5" dependencies: "@babel/code-frame": ^7.27.1 - "@babel/generator": ^7.28.3 + "@babel/generator": ^7.28.5 "@babel/helper-compilation-targets": ^7.27.2 "@babel/helper-module-transforms": ^7.28.3 "@babel/helpers": ^7.28.4 - "@babel/parser": ^7.28.4 + "@babel/parser": ^7.28.5 "@babel/template": ^7.27.2 - "@babel/traverse": ^7.28.4 - "@babel/types": ^7.28.4 + "@babel/traverse": ^7.28.5 + "@babel/types": ^7.28.5 "@jridgewell/remapping": ^2.3.5 convert-source-map: ^2.0.0 debug: ^4.1.0 gensync: ^1.0.0-beta.2 json5: ^2.2.3 semver: ^6.3.1 - checksum: f55b90b2c61a6461f5c0ccab74d32af9c67448c43c629529ba7ec3c61d87fa8c408cc9305bfb1f5b09e671d25436d44eaf75c48dee5dc0a5c5e21c01290f5134 + checksum: 1ee35b20448f73e9d531091ad4f9e8198dc8f0cebb783263fbff1807342209882ddcaf419be04111326b6f0e494222f7055d71da316c437a6a784d230c11ab9f languageName: node linkType: hard "@babel/eslint-parser@npm:^7.16.3": - version: 7.28.4 - resolution: "@babel/eslint-parser@npm:7.28.4" + version: 7.28.5 + resolution: "@babel/eslint-parser@npm:7.28.5" dependencies: "@nicolo-ribaudo/eslint-scope-5-internals": 5.1.1-v1 eslint-visitor-keys: ^2.1.0 @@ -576,20 +592,20 @@ __metadata: peerDependencies: "@babel/core": ^7.11.0 eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 - checksum: 32fb41c8648e169bc8570e25b4657475e165e1a49b8c6610f9bf86f311bbcc8dfa73f004977015688763aa6af17f48c690720bbc7b7b99695557adb267a9a7ff + checksum: 8daaf6f24d3f78c18bc4cf2bf1bedda3d829f330f385b85acf630adde3de7a703abf0d2615afea09244caa713dded01aa3c00f3637ea70568b2e8c547067fb99 languageName: node linkType: hard -"@babel/generator@npm:^7.28.3, @babel/generator@npm:^7.7.2": - version: 7.28.3 - resolution: "@babel/generator@npm:7.28.3" +"@babel/generator@npm:^7.28.5, @babel/generator@npm:^7.7.2": + version: 7.28.5 + resolution: "@babel/generator@npm:7.28.5" dependencies: - "@babel/parser": ^7.28.3 - "@babel/types": ^7.28.2 + "@babel/parser": ^7.28.5 + "@babel/types": ^7.28.5 "@jridgewell/gen-mapping": ^0.3.12 "@jridgewell/trace-mapping": ^0.3.28 jsesc: ^3.0.2 - checksum: e2202bf2b9c8a94f7e7a0a049fda0ee037d055c46922e85afa3bbc53309113f859b8193894f991045d7865226028b8f4f06152ed315ab414451932016dba5e42 + checksum: 3e86fa0197bb33394a85a73dbbca92bb1b3f250a30294c7e327359c0978ad90f36f3d71c7f2965a3fc349cfa82becc8f87e7421c75796c8bc48dd9010dd866d1 languageName: node linkType: hard @@ -615,33 +631,33 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.21.0, @babel/helper-create-class-features-plugin@npm:^7.27.1, @babel/helper-create-class-features-plugin@npm:^7.28.3": - version: 7.28.3 - resolution: "@babel/helper-create-class-features-plugin@npm:7.28.3" +"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.21.0, @babel/helper-create-class-features-plugin@npm:^7.27.1, @babel/helper-create-class-features-plugin@npm:^7.28.3, @babel/helper-create-class-features-plugin@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/helper-create-class-features-plugin@npm:7.28.5" dependencies: "@babel/helper-annotate-as-pure": ^7.27.3 - "@babel/helper-member-expression-to-functions": ^7.27.1 + "@babel/helper-member-expression-to-functions": ^7.28.5 "@babel/helper-optimise-call-expression": ^7.27.1 "@babel/helper-replace-supers": ^7.27.1 "@babel/helper-skip-transparent-expression-wrappers": ^7.27.1 - "@babel/traverse": ^7.28.3 + "@babel/traverse": ^7.28.5 semver: ^6.3.1 peerDependencies: "@babel/core": ^7.0.0 - checksum: 6d918e5e9c88ad1a262ab7b1a3caede1bbf95f8276c96846d8b0c1af251c85a0c868a9f1bbbaebdeb199e44dfd0e10fbe22935e56bedd1aa41ba4a7162bfa86c + checksum: 98f94a27bcde0cf0b847c41e1307057a1caddd131fb5fa0b1566e0c15ccc20b0ebab9667d782bffcd3eac9262226b18e86dcf30ab0f4dc5d14b1e1bf243aba49 languageName: node linkType: hard "@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-create-regexp-features-plugin@npm:7.27.1" + version: 7.28.5 + resolution: "@babel/helper-create-regexp-features-plugin@npm:7.28.5" dependencies: - "@babel/helper-annotate-as-pure": ^7.27.1 - regexpu-core: ^6.2.0 + "@babel/helper-annotate-as-pure": ^7.27.3 + regexpu-core: ^6.3.1 semver: ^6.3.1 peerDependencies: "@babel/core": ^7.0.0 - checksum: 2ede6bbad0016a9262fd281ce8f1a5d69e6179dcec4ea282830e924c29a29b66b0544ecb92e4ef4acdaf2c4c990931d7dc442dbcd6a8bcec4bad73923ef70934 + checksum: de202103e6ff8cd8da0d62eb269fcceb29857f3fa16173f0ff38188fd514e9ad4901aef1d590ff8ba25381644b42eaf70ad9ba91fda59fe7aa6a5e694cdde267 languageName: node linkType: hard @@ -667,13 +683,13 @@ __metadata: languageName: node linkType: hard -"@babel/helper-member-expression-to-functions@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-member-expression-to-functions@npm:7.27.1" +"@babel/helper-member-expression-to-functions@npm:^7.27.1, @babel/helper-member-expression-to-functions@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/helper-member-expression-to-functions@npm:7.28.5" dependencies: - "@babel/traverse": ^7.27.1 - "@babel/types": ^7.27.1 - checksum: b13a3d120015a6fd2f6e6c2ff789cd12498745ef028710cba612cfb751b91ace700c3f96c1689228d1dcb41e9d4cf83d6dff8627dcb0c8da12d79440e783c6b8 + "@babel/traverse": ^7.28.5 + "@babel/types": ^7.28.5 + checksum: 447d385233bae2eea713df1785f819b5a5ca272950740da123c42d23f491045120f0fbbb5609c091f7a9bbd40f289a442846dde0cb1bf0c59440fa093690cf7c languageName: node linkType: hard @@ -759,10 +775,10 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.25.9, @babel/helper-validator-identifier@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-validator-identifier@npm:7.27.1" - checksum: 3c7e8391e59d6c85baeefe9afb86432f2ab821c6232b00ea9082a51d3e7e95a2f3fb083d74dc1f49ac82cf238e1d2295dafcb001f7b0fab479f3f56af5eaaa47 +"@babel/helper-validator-identifier@npm:^7.25.9, @babel/helper-validator-identifier@npm:^7.27.1, @babel/helper-validator-identifier@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/helper-validator-identifier@npm:7.28.5" + checksum: 5a251a6848e9712aea0338f659a1a3bd334d26219d5511164544ca8ec20774f098c3a6661e9da65a0d085c745c00bb62c8fada38a62f08fa1f8053bc0aeb57e4 languageName: node linkType: hard @@ -806,26 +822,26 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.3, @babel/parser@npm:^7.28.4": - version: 7.28.4 - resolution: "@babel/parser@npm:7.28.4" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/parser@npm:7.28.5" dependencies: - "@babel/types": ^7.28.4 + "@babel/types": ^7.28.5 bin: parser: ./bin/babel-parser.js - checksum: d95e283fe1153039b396926ef567ca1ab114afb5c732a23bbcbbd0465ac59971aeb6a63f37593ce7671a52d34ec52b23008c999d68241b42d26928c540464063 + checksum: 5c2456e3f26c70d4a3ce1a220b529a91a2df26c54a2894fd0dea2342699ea1067ffdda9f0715eeab61da46ff546fd5661bc70be6d8d11977cbe21f5f0478819a languageName: node linkType: hard -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.27.1" +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.28.5" dependencies: "@babel/helper-plugin-utils": ^7.27.1 - "@babel/traverse": ^7.27.1 + "@babel/traverse": ^7.28.5 peerDependencies: "@babel/core": ^7.0.0 - checksum: 72f24b9487e445fa61cf8be552aad394a648c2bb445c38d39d1df003186d9685b87dd8d388c950f438ea0ca44c82099d9c49252fb681c719cc72edf02bbe0304 + checksum: 749b40a963d5633f554cad0336245cb6c1c1393c70a3fddcf302d86a1a42b35efdd2ed62056b88db66f3900887ae1cee9a3eeec89799c22e0cf65059f0dfd142 languageName: node linkType: hard @@ -1253,14 +1269,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoping@npm:^7.28.0": - version: 7.28.4 - resolution: "@babel/plugin-transform-block-scoping@npm:7.28.4" +"@babel/plugin-transform-block-scoping@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-transform-block-scoping@npm:7.28.5" dependencies: "@babel/helper-plugin-utils": ^7.27.1 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 7f62eae907c0b4f85b9cc024da949697e57d17f2107ca4a240011174762d4c546b856ccbd5ba83ecb4bc9eb50150ea46558d551a5b05d3f25aace88a65fa4e04 + checksum: 2cbc11c9b61097b61806c279211a4c4f5e85a5ca7fd52228efbf3a729178d330142a00a93695dbacc2898477e5fa9e34e7637f18323247ebebb84f43005560f3 languageName: node linkType: hard @@ -1288,7 +1304,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.28.3": +"@babel/plugin-transform-classes@npm:^7.28.4": version: 7.28.4 resolution: "@babel/plugin-transform-classes@npm:7.28.4" dependencies: @@ -1316,15 +1332,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-destructuring@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/plugin-transform-destructuring@npm:7.28.0" +"@babel/plugin-transform-destructuring@npm:^7.28.0, @babel/plugin-transform-destructuring@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-transform-destructuring@npm:7.28.5" dependencies: "@babel/helper-plugin-utils": ^7.27.1 - "@babel/traverse": ^7.28.0 + "@babel/traverse": ^7.28.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 5b464d6a03c6eaa1327b60ffc1630ca977db0256938b34e281e65c81c965680e930a6bac043272942d6d4bbd7d1eddded0b7231779429ba51275e092e7367859 + checksum: 74a06e55e715cfda0fdd8be53d2655d64dfdc28dffaede329d42548fd5b1449ad26a4ce43a24c3fd277b96f8b2010c7b3915afa8297911cda740cc5cc3a81f38 languageName: node linkType: hard @@ -1386,14 +1402,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-exponentiation-operator@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.27.1" +"@babel/plugin-transform-exponentiation-operator@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.28.5" dependencies: "@babel/helper-plugin-utils": ^7.27.1 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 4ff4a0f30babc457a5ae8564deda209599627c2ce647284a0e8e66f65b44f6d968cf77761a4cc31b45b61693f0810479248c79e681681d8ccb39d0c52944c1fd + checksum: da9bb5acd35c9fba92b802641f9462b82334158a149c78a739a04576a1e62be41057a201a41c022dda263bb73ac1a26521bbc997c7fc067f54d487af297995f4 languageName: node linkType: hard @@ -1467,14 +1483,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-logical-assignment-operators@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.27.1" +"@babel/plugin-transform-logical-assignment-operators@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.28.5" dependencies: "@babel/helper-plugin-utils": ^7.27.1 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 2757955d81d65cc4701c17b83720745f6858f7a1d1d58117e379c204f47adbeb066b778596b6168bdbf4a22c229aab595d79a9abc261d0c6bfd62d4419466e73 + checksum: c76778f4b186cc4f0b7e3658d91c690678bdb2b9d032f189213016d6177f2564709b79b386523b022b7d52e52331fd91f280f7c7bf85d835e0758b4b0d371447 languageName: node linkType: hard @@ -1513,17 +1529,17 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-systemjs@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-modules-systemjs@npm:7.27.1" +"@babel/plugin-transform-modules-systemjs@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.28.5" dependencies: - "@babel/helper-module-transforms": ^7.27.1 + "@babel/helper-module-transforms": ^7.28.3 "@babel/helper-plugin-utils": ^7.27.1 - "@babel/helper-validator-identifier": ^7.27.1 - "@babel/traverse": ^7.27.1 + "@babel/helper-validator-identifier": ^7.28.5 + "@babel/traverse": ^7.28.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 7c17a8973676c18525d87f277944616596f1b154cc2b9263bfd78ecdbf5f4288ec46c7f58017321ca3e3d6dfeb96875467b95311a39719b475d42a157525d87f + checksum: 646748dcf968c107fedfbff38aa37f7a9ebf2ccdf51fd9f578c6cd323371db36bbc5fe0d995544db168f39be9bca32a85fbf3bfff4742d2bed22e21c2847fa46 languageName: node linkType: hard @@ -1595,7 +1611,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-object-rest-spread@npm:^7.28.0": +"@babel/plugin-transform-object-rest-spread@npm:^7.28.4": version: 7.28.4 resolution: "@babel/plugin-transform-object-rest-spread@npm:7.28.4" dependencies: @@ -1633,15 +1649,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-chaining@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-optional-chaining@npm:7.27.1" +"@babel/plugin-transform-optional-chaining@npm:^7.27.1, @babel/plugin-transform-optional-chaining@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.28.5" dependencies: "@babel/helper-plugin-utils": ^7.27.1 "@babel/helper-skip-transparent-expression-wrappers": ^7.27.1 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: c4428d31f182d724db6f10575669aad3dbccceb0dea26aa9071fa89f11b3456278da3097fcc78937639a13c105a82cd452dc0218ce51abdbcf7626a013b928a5 + checksum: 78c2be52b32e893c992aca52ef84130b3540e2ca0e1ff0e45f8d2ccc213b3c6e2b43f8dd2c86a0976acf3cdff97d4488c23b86d7a3e67daa517013089f44af1d languageName: node linkType: hard @@ -1703,7 +1719,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-display-name@npm:^7.16.0, @babel/plugin-transform-react-display-name@npm:^7.27.1": +"@babel/plugin-transform-react-display-name@npm:^7.16.0, @babel/plugin-transform-react-display-name@npm:^7.28.0": version: 7.28.0 resolution: "@babel/plugin-transform-react-display-name@npm:7.28.0" dependencies: @@ -1774,7 +1790,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-regenerator@npm:^7.28.3": +"@babel/plugin-transform-regenerator@npm:^7.28.4": version: 7.28.4 resolution: "@babel/plugin-transform-regenerator@npm:7.28.4" dependencies: @@ -1809,8 +1825,8 @@ __metadata: linkType: hard "@babel/plugin-transform-runtime@npm:^7.16.4": - version: 7.28.3 - resolution: "@babel/plugin-transform-runtime@npm:7.28.3" + version: 7.28.5 + resolution: "@babel/plugin-transform-runtime@npm:7.28.5" dependencies: "@babel/helper-module-imports": ^7.27.1 "@babel/helper-plugin-utils": ^7.27.1 @@ -1820,7 +1836,7 @@ __metadata: semver: ^6.3.1 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 63d2fc05d5bfcb96f31be54b095d72a89f0a03c8de10f5d742b18b174e2731bcdc27292e8deec66c2e88cebf8298393123d5e767526f6fffbc75cb8144ef66c6 + checksum: 5bb66f366c5bb22d0c890667ecd0f1fde9db86ac04df62b21fc2bbf58531eb84068bb0bf38fb1c496c8f78a917c59a884f6c1f8b205b8689d155e72fcf1d442d languageName: node linkType: hard @@ -1880,18 +1896,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-typescript@npm:^7.27.1": - version: 7.28.0 - resolution: "@babel/plugin-transform-typescript@npm:7.28.0" +"@babel/plugin-transform-typescript@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-transform-typescript@npm:7.28.5" dependencies: "@babel/helper-annotate-as-pure": ^7.27.3 - "@babel/helper-create-class-features-plugin": ^7.27.1 + "@babel/helper-create-class-features-plugin": ^7.28.5 "@babel/helper-plugin-utils": ^7.27.1 "@babel/helper-skip-transparent-expression-wrappers": ^7.27.1 "@babel/plugin-syntax-typescript": ^7.27.1 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 14c1024bcd57fcd469d90cf0c15c3cd4e771e2eb2cd9afee3aa79b59c8ed103654f7c5c71cdb3bfe31c1d0cb08bfad8c80f5aa1d24b4b454bd21301d5925533d + checksum: 202785e9cc6fb04efba091b3d5560cc8089cdc54df12fafa3d32ed7089e8d7a95b92b2fb1b53ec3e4db3bbafe56e8b32a3530cac004b3e493e902def8666001d languageName: node linkType: hard @@ -1943,14 +1959,14 @@ __metadata: linkType: hard "@babel/preset-env@npm:^7.11.0, @babel/preset-env@npm:^7.12.1, @babel/preset-env@npm:^7.16.4": - version: 7.28.3 - resolution: "@babel/preset-env@npm:7.28.3" + version: 7.28.5 + resolution: "@babel/preset-env@npm:7.28.5" dependencies: - "@babel/compat-data": ^7.28.0 + "@babel/compat-data": ^7.28.5 "@babel/helper-compilation-targets": ^7.27.2 "@babel/helper-plugin-utils": ^7.27.1 "@babel/helper-validator-option": ^7.27.1 - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ^7.27.1 + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ^7.28.5 "@babel/plugin-bugfix-safari-class-field-initializer-scope": ^7.27.1 "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.27.1 "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.27.1 @@ -1963,42 +1979,42 @@ __metadata: "@babel/plugin-transform-async-generator-functions": ^7.28.0 "@babel/plugin-transform-async-to-generator": ^7.27.1 "@babel/plugin-transform-block-scoped-functions": ^7.27.1 - "@babel/plugin-transform-block-scoping": ^7.28.0 + "@babel/plugin-transform-block-scoping": ^7.28.5 "@babel/plugin-transform-class-properties": ^7.27.1 "@babel/plugin-transform-class-static-block": ^7.28.3 - "@babel/plugin-transform-classes": ^7.28.3 + "@babel/plugin-transform-classes": ^7.28.4 "@babel/plugin-transform-computed-properties": ^7.27.1 - "@babel/plugin-transform-destructuring": ^7.28.0 + "@babel/plugin-transform-destructuring": ^7.28.5 "@babel/plugin-transform-dotall-regex": ^7.27.1 "@babel/plugin-transform-duplicate-keys": ^7.27.1 "@babel/plugin-transform-duplicate-named-capturing-groups-regex": ^7.27.1 "@babel/plugin-transform-dynamic-import": ^7.27.1 "@babel/plugin-transform-explicit-resource-management": ^7.28.0 - "@babel/plugin-transform-exponentiation-operator": ^7.27.1 + "@babel/plugin-transform-exponentiation-operator": ^7.28.5 "@babel/plugin-transform-export-namespace-from": ^7.27.1 "@babel/plugin-transform-for-of": ^7.27.1 "@babel/plugin-transform-function-name": ^7.27.1 "@babel/plugin-transform-json-strings": ^7.27.1 "@babel/plugin-transform-literals": ^7.27.1 - "@babel/plugin-transform-logical-assignment-operators": ^7.27.1 + "@babel/plugin-transform-logical-assignment-operators": ^7.28.5 "@babel/plugin-transform-member-expression-literals": ^7.27.1 "@babel/plugin-transform-modules-amd": ^7.27.1 "@babel/plugin-transform-modules-commonjs": ^7.27.1 - "@babel/plugin-transform-modules-systemjs": ^7.27.1 + "@babel/plugin-transform-modules-systemjs": ^7.28.5 "@babel/plugin-transform-modules-umd": ^7.27.1 "@babel/plugin-transform-named-capturing-groups-regex": ^7.27.1 "@babel/plugin-transform-new-target": ^7.27.1 "@babel/plugin-transform-nullish-coalescing-operator": ^7.27.1 "@babel/plugin-transform-numeric-separator": ^7.27.1 - "@babel/plugin-transform-object-rest-spread": ^7.28.0 + "@babel/plugin-transform-object-rest-spread": ^7.28.4 "@babel/plugin-transform-object-super": ^7.27.1 "@babel/plugin-transform-optional-catch-binding": ^7.27.1 - "@babel/plugin-transform-optional-chaining": ^7.27.1 + "@babel/plugin-transform-optional-chaining": ^7.28.5 "@babel/plugin-transform-parameters": ^7.27.7 "@babel/plugin-transform-private-methods": ^7.27.1 "@babel/plugin-transform-private-property-in-object": ^7.27.1 "@babel/plugin-transform-property-literals": ^7.27.1 - "@babel/plugin-transform-regenerator": ^7.28.3 + "@babel/plugin-transform-regenerator": ^7.28.4 "@babel/plugin-transform-regexp-modifiers": ^7.27.1 "@babel/plugin-transform-reserved-words": ^7.27.1 "@babel/plugin-transform-shorthand-properties": ^7.27.1 @@ -2018,7 +2034,7 @@ __metadata: semver: ^6.3.1 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: c4e70f69b727d21eedd4de201ac082e951482f2d28a388e401e7937fd6f15bc1a49a63c12f59e87a18d237ac037a5b29d983f3bb82f1196d6444ae5b605ac6e2 + checksum: 9e17ba89c5d8cbea0fde564ea29e6dc17ad43f6ebf1c11347af69a04cf69dbc62c3124d2afe46412bfa41dddde3aaabfeffc0d68bed96f6ea0c4d8fbf652e761 languageName: node linkType: hard @@ -2036,33 +2052,33 @@ __metadata: linkType: hard "@babel/preset-react@npm:^7.12.5, @babel/preset-react@npm:^7.16.0, @babel/preset-react@npm:^7.18.6": - version: 7.27.1 - resolution: "@babel/preset-react@npm:7.27.1" + version: 7.28.5 + resolution: "@babel/preset-react@npm:7.28.5" dependencies: "@babel/helper-plugin-utils": ^7.27.1 "@babel/helper-validator-option": ^7.27.1 - "@babel/plugin-transform-react-display-name": ^7.27.1 + "@babel/plugin-transform-react-display-name": ^7.28.0 "@babel/plugin-transform-react-jsx": ^7.27.1 "@babel/plugin-transform-react-jsx-development": ^7.27.1 "@babel/plugin-transform-react-pure-annotations": ^7.27.1 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 00bc146f9c742eed804c598d3f31b7d889c1baf8c768989b7f84a93ca527dd1518d3b86781e89ca45cae6dbee136510d3a121658e01416c5578aecf751517bb5 + checksum: 13bc1fe4dde0a29d00323e46749e5beb457844507cb3afa2fefbd85d283c2d4836f9e4a780be735de58a44c505870476dc2838f1f8faf9d6f056481e65f1a0fb languageName: node linkType: hard "@babel/preset-typescript@npm:^7.16.0, @babel/preset-typescript@npm:^7.18.6": - version: 7.27.1 - resolution: "@babel/preset-typescript@npm:7.27.1" + version: 7.28.5 + resolution: "@babel/preset-typescript@npm:7.28.5" dependencies: "@babel/helper-plugin-utils": ^7.27.1 "@babel/helper-validator-option": ^7.27.1 "@babel/plugin-syntax-jsx": ^7.27.1 "@babel/plugin-transform-modules-commonjs": ^7.27.1 - "@babel/plugin-transform-typescript": ^7.27.1 + "@babel/plugin-transform-typescript": ^7.28.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 38020f1b23e88ec4fbffd5737da455d8939244bddfb48a2516aef93fb5947bd9163fb807ce6eff3e43fa5ffe9113aa131305fef0fb5053998410bbfcfe6ce0ec + checksum: 22f889835d9db1e627846e71ca2f02e2d24e2eb9ebcf9845b3b1d37bd3a53787967bafabbbcb342f06aaf7627399a7102ba6ca18f9a0e17066c865d680d2ceb9 languageName: node linkType: hard @@ -2084,28 +2100,28 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4, @babel/traverse@npm:^7.7.2": - version: 7.28.4 - resolution: "@babel/traverse@npm:7.28.4" +"@babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4, @babel/traverse@npm:^7.28.5, @babel/traverse@npm:^7.7.2": + version: 7.28.5 + resolution: "@babel/traverse@npm:7.28.5" dependencies: "@babel/code-frame": ^7.27.1 - "@babel/generator": ^7.28.3 + "@babel/generator": ^7.28.5 "@babel/helper-globals": ^7.28.0 - "@babel/parser": ^7.28.4 + "@babel/parser": ^7.28.5 "@babel/template": ^7.27.2 - "@babel/types": ^7.28.4 + "@babel/types": ^7.28.5 debug: ^4.3.1 - checksum: d603b8ce4e55ba4fc7b28d3362cc2b1b20bc887e471c8a59fe87b2578c26803c9ef8fcd118081dd8283ea78e0e9a6df9d88c8520033c6aaf81eec30d2a669151 + checksum: e028ee9654f44be7c2a2df268455cee72d5c424c9ae536785f8f7c8680356f7b977c77ad76909d07eeed09ff1e125ce01cf783011f66b56c838791a85fa6af04 languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": - version: 7.28.4 - resolution: "@babel/types@npm:7.28.4" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": + version: 7.28.5 + resolution: "@babel/types@npm:7.28.5" dependencies: "@babel/helper-string-parser": ^7.27.1 - "@babel/helper-validator-identifier": ^7.27.1 - checksum: a369b4fb73415a2ed902a15576b49696ae9777ddee394a7a904c62e6fbb31f43906b0147ae0b8f03ac17f20c248eac093df349e33c65c94617b12e524b759694 + "@babel/helper-validator-identifier": ^7.28.5 + checksum: 5bc266af9e55ff92f9ddf33d83a42c9de1a87f9579d0ed62ef94a741a081692dd410a4fbbab18d514b83e135083ff05bc0e37003834801c9514b9d8ad748070d languageName: node linkType: hard @@ -2458,9 +2474,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/aix-ppc64@npm:0.25.11" +"@esbuild/aix-ppc64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/aix-ppc64@npm:0.25.12" conditions: os=aix & cpu=ppc64 languageName: node linkType: hard @@ -2486,9 +2502,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/android-arm64@npm:0.25.11" +"@esbuild/android-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/android-arm64@npm:0.25.12" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -2514,9 +2530,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/android-arm@npm:0.25.11" +"@esbuild/android-arm@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/android-arm@npm:0.25.12" conditions: os=android & cpu=arm languageName: node linkType: hard @@ -2542,9 +2558,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/android-x64@npm:0.25.11" +"@esbuild/android-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/android-x64@npm:0.25.12" conditions: os=android & cpu=x64 languageName: node linkType: hard @@ -2570,9 +2586,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/darwin-arm64@npm:0.25.11" +"@esbuild/darwin-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/darwin-arm64@npm:0.25.12" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -2598,9 +2614,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/darwin-x64@npm:0.25.11" +"@esbuild/darwin-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/darwin-x64@npm:0.25.12" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -2626,9 +2642,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/freebsd-arm64@npm:0.25.11" +"@esbuild/freebsd-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/freebsd-arm64@npm:0.25.12" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard @@ -2654,9 +2670,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/freebsd-x64@npm:0.25.11" +"@esbuild/freebsd-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/freebsd-x64@npm:0.25.12" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -2682,9 +2698,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/linux-arm64@npm:0.25.11" +"@esbuild/linux-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-arm64@npm:0.25.12" conditions: os=linux & cpu=arm64 languageName: node linkType: hard @@ -2710,9 +2726,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/linux-arm@npm:0.25.11" +"@esbuild/linux-arm@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-arm@npm:0.25.12" conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -2738,9 +2754,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/linux-ia32@npm:0.25.11" +"@esbuild/linux-ia32@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-ia32@npm:0.25.12" conditions: os=linux & cpu=ia32 languageName: node linkType: hard @@ -2766,9 +2782,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/linux-loong64@npm:0.25.11" +"@esbuild/linux-loong64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-loong64@npm:0.25.12" conditions: os=linux & cpu=loong64 languageName: node linkType: hard @@ -2794,9 +2810,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/linux-mips64el@npm:0.25.11" +"@esbuild/linux-mips64el@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-mips64el@npm:0.25.12" conditions: os=linux & cpu=mips64el languageName: node linkType: hard @@ -2822,9 +2838,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/linux-ppc64@npm:0.25.11" +"@esbuild/linux-ppc64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-ppc64@npm:0.25.12" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard @@ -2850,9 +2866,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/linux-riscv64@npm:0.25.11" +"@esbuild/linux-riscv64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-riscv64@npm:0.25.12" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard @@ -2878,9 +2894,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/linux-s390x@npm:0.25.11" +"@esbuild/linux-s390x@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-s390x@npm:0.25.12" conditions: os=linux & cpu=s390x languageName: node linkType: hard @@ -2906,9 +2922,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/linux-x64@npm:0.25.11" +"@esbuild/linux-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-x64@npm:0.25.12" conditions: os=linux & cpu=x64 languageName: node linkType: hard @@ -2920,9 +2936,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/netbsd-arm64@npm:0.25.11" +"@esbuild/netbsd-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/netbsd-arm64@npm:0.25.12" conditions: os=netbsd & cpu=arm64 languageName: node linkType: hard @@ -2948,9 +2964,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/netbsd-x64@npm:0.25.11" +"@esbuild/netbsd-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/netbsd-x64@npm:0.25.12" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard @@ -2962,9 +2978,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/openbsd-arm64@npm:0.25.11" +"@esbuild/openbsd-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/openbsd-arm64@npm:0.25.12" conditions: os=openbsd & cpu=arm64 languageName: node linkType: hard @@ -2990,16 +3006,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/openbsd-x64@npm:0.25.11" +"@esbuild/openbsd-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/openbsd-x64@npm:0.25.12" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/openharmony-arm64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/openharmony-arm64@npm:0.25.11" +"@esbuild/openharmony-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/openharmony-arm64@npm:0.25.12" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard @@ -3025,9 +3041,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/sunos-x64@npm:0.25.11" +"@esbuild/sunos-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/sunos-x64@npm:0.25.12" conditions: os=sunos & cpu=x64 languageName: node linkType: hard @@ -3053,9 +3069,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/win32-arm64@npm:0.25.11" +"@esbuild/win32-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/win32-arm64@npm:0.25.12" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -3081,9 +3097,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/win32-ia32@npm:0.25.11" +"@esbuild/win32-ia32@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/win32-ia32@npm:0.25.12" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard @@ -3109,9 +3125,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.25.11": - version: 0.25.11 - resolution: "@esbuild/win32-x64@npm:0.25.11" +"@esbuild/win32-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/win32-x64@npm:0.25.12" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -3128,9 +3144,9 @@ __metadata: linkType: hard "@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.4.0, @eslint-community/regexpp@npm:^4.6.1": - version: 4.12.1 - resolution: "@eslint-community/regexpp@npm:4.12.1" - checksum: 0d628680e204bc316d545b4993d3658427ca404ae646ce541fcc65306b8c712c340e5e573e30fb9f85f4855c0c5f6dca9868931f2fcced06417fbe1a0c6cd2d6 + version: 4.12.2 + resolution: "@eslint-community/regexpp@npm:4.12.2" + checksum: 1770bc81f676a72f65c7200b5675ff7a349786521f30e66125faaf767fde1ba1c19c3790e16ba8508a62a3933afcfc806a893858b3b5906faf693d862b9e4120 languageName: node linkType: hard @@ -3283,65 +3299,65 @@ __metadata: languageName: node linkType: hard -"@inquirer/ansi@npm:^1.0.1": - version: 1.0.1 - resolution: "@inquirer/ansi@npm:1.0.1" - checksum: 0dda65720736f3e730715f3778e0e90f039ebd1382c277495a4d1cdbd2b2863095aa7291cd8ea7d3c0618bdee04a375db6e10a7bae5fb904df0b632a1c7774f9 +"@inquirer/ansi@npm:^1.0.2": + version: 1.0.2 + resolution: "@inquirer/ansi@npm:1.0.2" + checksum: d1496e573a63ee6752bcf3fc93375cdabc55b0d60f0588fe7902282c710b223252ad318ff600ee904e48555634663b53fda517f5b29ce9fbda90bfae18592fbc languageName: node linkType: hard "@inquirer/confirm@npm:^5.0.0": - version: 5.1.19 - resolution: "@inquirer/confirm@npm:5.1.19" + version: 5.1.21 + resolution: "@inquirer/confirm@npm:5.1.21" dependencies: - "@inquirer/core": ^10.3.0 - "@inquirer/type": ^3.0.9 + "@inquirer/core": ^10.3.2 + "@inquirer/type": ^3.0.10 peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: d65e0addf80c146d71a74057d77048bd78a4a80d74a9e0d774b759ff1adf38a33cde6c06a6d6ef802bb61ef9158770315dec3931f89b3624c0e63c595c0473c1 + checksum: a107aa0073965ea510affb9e5b55baf40333503d600970c458c07770cd4e0eee01efc4caba66f0409b0fadc9550d127329622efb543cffcabff3ad0e7f865372 languageName: node linkType: hard -"@inquirer/core@npm:^10.3.0": - version: 10.3.0 - resolution: "@inquirer/core@npm:10.3.0" +"@inquirer/core@npm:^10.3.2": + version: 10.3.2 + resolution: "@inquirer/core@npm:10.3.2" dependencies: - "@inquirer/ansi": ^1.0.1 - "@inquirer/figures": ^1.0.14 - "@inquirer/type": ^3.0.9 + "@inquirer/ansi": ^1.0.2 + "@inquirer/figures": ^1.0.15 + "@inquirer/type": ^3.0.10 cli-width: ^4.1.0 mute-stream: ^2.0.0 signal-exit: ^4.1.0 wrap-ansi: ^6.2.0 - yoctocolors-cjs: ^2.1.2 + yoctocolors-cjs: ^2.1.3 peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 42607c2e8388bf6505f5ce1716d47750f9386085f3080733b7f27bfe59d576d480ec622d7468fcf1bd9b854ff117311421d9eae0c083873c67324023635e103a + checksum: ca820e798e02b1d4aff2ad4a8057739abf4140918592ff8ab179f774cdbe51916f24267631e86741a85a48cfa1a08666149785b5e2437ca4b18ef10938486017 languageName: node linkType: hard -"@inquirer/figures@npm:^1.0.14": - version: 1.0.14 - resolution: "@inquirer/figures@npm:1.0.14" - checksum: 37eec986f119eabb6c231c8c1481c6a48ab2347e9f57b2d6442161f7b83936678221fccb7ead60582026c2ae20d457467d0727c485ff53aee2cf965077b0f51b +"@inquirer/figures@npm:^1.0.15": + version: 1.0.15 + resolution: "@inquirer/figures@npm:1.0.15" + checksum: bd87a578ab667236cb72bdbb900cb144017dbc306d60e9dc7e665cd7d6b3097e9464cb4d8fe215315083a7820530caf86d7af59e7c41a35a555fb22a881913ad languageName: node linkType: hard -"@inquirer/type@npm:^3.0.9": - version: 3.0.9 - resolution: "@inquirer/type@npm:3.0.9" +"@inquirer/type@npm:^3.0.10": + version: 3.0.10 + resolution: "@inquirer/type@npm:3.0.10" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 960ba4737405f70bac17e7cdc4696c60064b06c8dd13a4b3d0783763ba1714bdadbd598b88d537ab9415b7d5d61e011ac042cfbd1438b2a35298e2868724b853 + checksum: 57d113a9db7abc73326491e29bedc88ef362e53779f9f58a1b61225e0be068ce0c54e33cd65f4a13ca46131676fb72c3ef488463c4c9af0aa89680684c55d74c languageName: node linkType: hard @@ -3361,20 +3377,6 @@ __metadata: languageName: node linkType: hard -"@isaacs/cliui@npm:^8.0.2": - version: 8.0.2 - resolution: "@isaacs/cliui@npm:8.0.2" - dependencies: - string-width: ^5.1.2 - string-width-cjs: "npm:string-width@^4.2.0" - strip-ansi: ^7.0.1 - strip-ansi-cjs: "npm:strip-ansi@^6.0.1" - wrap-ansi: ^8.1.0 - wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" - checksum: 4a473b9b32a7d4d3cfb7a614226e555091ff0c5a29a1734c28c72a182c2f6699b26fc6b5c2131dfd841e86b185aea714c72201d7c98c2fba5f17709333a67aeb - languageName: node - linkType: hard - "@isaacs/fs-minipass@npm:^4.0.0": version: 4.0.1 resolution: "@isaacs/fs-minipass@npm:4.0.1" @@ -3771,9 +3773,9 @@ __metadata: languageName: node linkType: hard -"@mswjs/interceptors@npm:^0.39.1": - version: 0.39.8 - resolution: "@mswjs/interceptors@npm:0.39.8" +"@mswjs/interceptors@npm:^0.40.0": + version: 0.40.0 + resolution: "@mswjs/interceptors@npm:0.40.0" dependencies: "@open-draft/deferred-promise": ^2.2.0 "@open-draft/logger": ^0.3.0 @@ -3781,7 +3783,7 @@ __metadata: is-node-process: ^1.2.0 outvariant: ^1.4.3 strict-event-emitter: ^0.5.1 - checksum: c07dd5ffa7573ee2d77fd5047f06198f89b656791f7a4e2e3a67678e7f9f11f8b6ffe88429f2d48b251936b3529342644ff1b1c69ad16617bb7ca048140cc398 + checksum: b9d9bd80d2c26d4603a9bf449f5b1379cb039482cfa459e49290f3ddcafa6e4a75b5d9cd4a6283cee6af3df0fa13b7f2556958ef307b390f7e56961c05e2ab57 languageName: node linkType: hard @@ -3966,9 +3968,9 @@ __metadata: languageName: node linkType: hard -"@mui/types@npm:^7.2.15, @mui/types@npm:^7.2.17, @mui/types@npm:^7.4.7": - version: 7.4.7 - resolution: "@mui/types@npm:7.4.7" +"@mui/types@npm:^7.2.15, @mui/types@npm:^7.2.17, @mui/types@npm:^7.4.8": + version: 7.4.8 + resolution: "@mui/types@npm:7.4.8" dependencies: "@babel/runtime": ^7.28.4 peerDependencies: @@ -3976,7 +3978,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 2650dd5ef5f9fb1d0d7baca6e367f16e99955331f7200e01cdeffaf8dc875ddf3116f6edb840fbda32d3758bef8861904a7d2e3ac5994f137ee50bb7743b5d0b + checksum: 58c2c2612787b9af5b8b08ca7c1ffcae7acd28523e86f63d28c1683ef9e56b55e0dab3f4e8c89cc102c6f866cc8e107680f3aad71f516cbd717da0eecc8c46e1 languageName: node linkType: hard @@ -4033,22 +4035,22 @@ __metadata: linkType: hard "@mui/utils@npm:^5.16.6 || ^6.0.0 || ^7.0.0": - version: 7.3.3 - resolution: "@mui/utils@npm:7.3.3" + version: 7.3.5 + resolution: "@mui/utils@npm:7.3.5" dependencies: "@babel/runtime": ^7.28.4 - "@mui/types": ^7.4.7 + "@mui/types": ^7.4.8 "@types/prop-types": ^15.7.15 clsx: ^2.1.1 prop-types: ^15.8.1 - react-is: ^19.1.1 + react-is: ^19.2.0 peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: "@types/react": optional: true - checksum: ad9c6efebeaf75b83d3814724b71503e69bdf5ae0e71cf2256d697d17b592541ad1c28a91699a4cb5a7d580d2be9d690703149c410c26d40fc522048f82b6a06 + checksum: be2df33b3f2b7cb5d95a8b8f638af0d12e0f34eb8af26c52ce65c5f86f6a242748846b0e3770d1a779cee82387d4f650562584e1612d484808a6fdeabe214847 languageName: node linkType: hard @@ -4193,25 +4195,25 @@ __metadata: languageName: node linkType: hard -"@npmcli/agent@npm:^3.0.0": - version: 3.0.0 - resolution: "@npmcli/agent@npm:3.0.0" +"@npmcli/agent@npm:^4.0.0": + version: 4.0.0 + resolution: "@npmcli/agent@npm:4.0.0" dependencies: agent-base: ^7.1.0 http-proxy-agent: ^7.0.0 https-proxy-agent: ^7.0.1 - lru-cache: ^10.0.1 + lru-cache: ^11.2.1 socks-proxy-agent: ^8.0.3 - checksum: e8fc25d536250ed3e669813b36e8c6d805628b472353c57afd8c4fde0fcfcf3dda4ffe22f7af8c9070812ec2e7a03fb41d7151547cef3508efe661a5a3add20f + checksum: 89ae20b44859ff8d4de56ade319d8ceaa267a0742d6f7345fe98aa5cd8614ced7db85ea4dc5bfbd6614dbb200a10b134e087143582534c939e8a02219e8665c8 languageName: node linkType: hard -"@npmcli/fs@npm:^4.0.0": - version: 4.0.0 - resolution: "@npmcli/fs@npm:4.0.0" +"@npmcli/fs@npm:^5.0.0": + version: 5.0.0 + resolution: "@npmcli/fs@npm:5.0.0" dependencies: semver: ^7.3.5 - checksum: 68951c589e9a4328698a35fd82fe71909a257d6f2ede0434d236fa55634f0fbcad9bb8755553ce5849bd25ee6f019f4d435921ac715c853582c4a7f5983c8d4a + checksum: 897dac32eb37e011800112d406b9ea2ebd96f1dab01bb8fbeb59191b86f6825dffed6a89f3b6c824753d10f8735b76d630927bd7610e9e123b129ef2e5f02cb5 languageName: node linkType: hard @@ -4240,11 +4242,11 @@ __metadata: linkType: hard "@paralleldrive/cuid2@npm:^2.2.2": - version: 2.2.2 - resolution: "@paralleldrive/cuid2@npm:2.2.2" + version: 2.3.1 + resolution: "@paralleldrive/cuid2@npm:2.3.1" dependencies: "@noble/hashes": ^1.1.5 - checksum: f7f6ac70e0268ec2c72e555719240d5c2c9a859ce541ac1c637eed3f3ee971b42881d299dedafbded53e7365b9e98176c5a31c442c1112f7e9e7306f2fd0ecbb + checksum: 1310abcdf8f50721725bf7394f89cb6e704308cdfb62da6a251d93c036d4c4ae45987ed10dcf58d0ccdd1e8fdf0b860351306f582457b5146154cb1e660bbd39 languageName: node linkType: hard @@ -4410,13 +4412,6 @@ __metadata: languageName: node linkType: hard -"@pkgjs/parseargs@npm:^0.11.0": - version: 0.11.0 - resolution: "@pkgjs/parseargs@npm:0.11.0" - checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f - languageName: node - linkType: hard - "@pkgr/core@npm:^0.2.9": version: 0.2.9 resolution: "@pkgr/core@npm:0.2.9" @@ -4469,8 +4464,8 @@ __metadata: linkType: hard "@prisma/client@npm:^6.2.1": - version: 6.17.1 - resolution: "@prisma/client@npm:6.17.1" + version: 6.19.0 + resolution: "@prisma/client@npm:6.19.0" peerDependencies: prisma: "*" typescript: ">=5.1.0" @@ -4479,65 +4474,65 @@ __metadata: optional: true typescript: optional: true - checksum: 35160754c9e61252f662199fff7b35acf353eeee1faf0761b6efc5c1a610ccca6e7dcf89e3b8c2238318d0862a65214842bac432743da228336f6e0202a2acbc + checksum: 6f65d6f0b0874e167ab08d19056bc7487aa3fa98647ca1ae08f377f93a37c9b2c1f98dc3f0dd1a0a016690da3778f02710a367176fd1903cfacbc02cf85011ec languageName: node linkType: hard -"@prisma/config@npm:6.17.1": - version: 6.17.1 - resolution: "@prisma/config@npm:6.17.1" +"@prisma/config@npm:6.19.0": + version: 6.19.0 + resolution: "@prisma/config@npm:6.19.0" dependencies: c12: 3.1.0 deepmerge-ts: 7.1.5 - effect: 3.16.12 + effect: 3.18.4 empathic: 2.0.0 - checksum: f7138068402a70baca506681fee89bbe2458cc4092540f9b4d27b4c88e3c58c65b25ab48b852e8ad20b44259007b9214c45ef85565b9ef4cb6d39be026715a09 + checksum: 94b270cf53f1a3dc9442351f595b7d10f4b31667eddd1413f0bcdc1db4598e9370e3a8b22cb703b2fc642aa7fca477041b3cce4ce6d0b9398dc5c0a4ab4d00b2 languageName: node linkType: hard -"@prisma/debug@npm:6.17.1": - version: 6.17.1 - resolution: "@prisma/debug@npm:6.17.1" - checksum: 6b9a14f8b7d963a99834b6fe1f5c42c43d81cd57ef84de424eb2ebb9d03ad7ebc5a4e86a242afa0eab9bc72a9289d604039702e7c18efd3062fe485e5b32470d +"@prisma/debug@npm:6.19.0": + version: 6.19.0 + resolution: "@prisma/debug@npm:6.19.0" + checksum: 34c4421448715079132e32fbf394042ca5fcedb3ca578840480de57771c0dd80ceb06ec1fa0ff9157417bda87b1d4f4f2ab6af5cd972b83f48e9428d663c8cec languageName: node linkType: hard -"@prisma/engines-version@npm:6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac": - version: 6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac - resolution: "@prisma/engines-version@npm:6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac" - checksum: e076286cb701ab38a2591dce50c6d22e6217575559d387a6983908f27f917c0fae142217f3a91122bd572966265195d5d3ebcc41005e682c746b16027ac1460e +"@prisma/engines-version@npm:6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773": + version: 6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773 + resolution: "@prisma/engines-version@npm:6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773" + checksum: 2ef55683e372951729367a613d1a06eacc528f738cfc5d5c62021bc29160a11daeea3258d6533d896cad7224c47cf0fc96806aade27723a3383e5f13d64fd4d8 languageName: node linkType: hard -"@prisma/engines@npm:6.17.1": - version: 6.17.1 - resolution: "@prisma/engines@npm:6.17.1" +"@prisma/engines@npm:6.19.0": + version: 6.19.0 + resolution: "@prisma/engines@npm:6.19.0" dependencies: - "@prisma/debug": 6.17.1 - "@prisma/engines-version": 6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac - "@prisma/fetch-engine": 6.17.1 - "@prisma/get-platform": 6.17.1 - checksum: 841f6a8b315de2241657c2b8c53ecc2d7c0da04be566abb5f3ecb48db97433f3cf43586f694e0449fa9b4b6336b383de2595d727903933ab1a4bd685c4bd93ad + "@prisma/debug": 6.19.0 + "@prisma/engines-version": 6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773 + "@prisma/fetch-engine": 6.19.0 + "@prisma/get-platform": 6.19.0 + checksum: 98d131b4ff615d029c195c84e80faefe634f65c74b1f64a49409883770401b57345ac3b2d0b30ccab3aeb05da4cb3ca29648fbbd75ef7316f7dab7df04ad1be2 languageName: node linkType: hard -"@prisma/fetch-engine@npm:6.17.1": - version: 6.17.1 - resolution: "@prisma/fetch-engine@npm:6.17.1" +"@prisma/fetch-engine@npm:6.19.0": + version: 6.19.0 + resolution: "@prisma/fetch-engine@npm:6.19.0" dependencies: - "@prisma/debug": 6.17.1 - "@prisma/engines-version": 6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac - "@prisma/get-platform": 6.17.1 - checksum: 7c35ec4d6ac1834dbe43794fe1fa370c984d419b265fd7272cd76daf6a992b806b88d92b62c50cf7393f7ee9abc4e6fa5ebcf2d636824c363a0a05384ba4542f + "@prisma/debug": 6.19.0 + "@prisma/engines-version": 6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773 + "@prisma/get-platform": 6.19.0 + checksum: 72b86582ce68896350a94c3e4753456cfddc4e3685c08ad3ef8ec1de6c83bc5378525a6e31509450794d3c026c4c719ee5810bd91805e35e00537b8d5f54600c languageName: node linkType: hard -"@prisma/get-platform@npm:6.17.1": - version: 6.17.1 - resolution: "@prisma/get-platform@npm:6.17.1" +"@prisma/get-platform@npm:6.19.0": + version: 6.19.0 + resolution: "@prisma/get-platform@npm:6.19.0" dependencies: - "@prisma/debug": 6.17.1 - checksum: f3d0be484af74c9fc90a05397bdf06fb32898ff94137671b292544a7ba8504b6c16882a7e0709c9f407f4c2e48a4132521d1de1e2c4dfd677a35e2467d59569a + "@prisma/debug": 6.19.0 + checksum: 927ccbc40f3bdbe23a4f4c2876b309f69ce654aa0a5acfd9e1ddc740e79503405660cfeec637e3b84dac27a4dd6fe80f8dc5437b659309274ee1f46075024c49 languageName: node linkType: hard @@ -4616,156 +4611,156 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.52.4" +"@rollup/rollup-android-arm-eabi@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.53.3" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-android-arm64@npm:4.52.4" +"@rollup/rollup-android-arm64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-android-arm64@npm:4.53.3" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-darwin-arm64@npm:4.52.4" +"@rollup/rollup-darwin-arm64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-darwin-arm64@npm:4.53.3" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-darwin-x64@npm:4.52.4" +"@rollup/rollup-darwin-x64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-darwin-x64@npm:4.53.3" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.52.4" +"@rollup/rollup-freebsd-arm64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.53.3" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-freebsd-x64@npm:4.52.4" +"@rollup/rollup-freebsd-x64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-freebsd-x64@npm:4.53.3" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.52.4" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.53.3" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.52.4" +"@rollup/rollup-linux-arm-musleabihf@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.53.3" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.52.4" +"@rollup/rollup-linux-arm64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.53.3" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.52.4" +"@rollup/rollup-linux-arm64-musl@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.53.3" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loong64-gnu@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.52.4" +"@rollup/rollup-linux-loong64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.53.3" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-ppc64-gnu@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.52.4" +"@rollup/rollup-linux-ppc64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.53.3" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.52.4" +"@rollup/rollup-linux-riscv64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.53.3" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-musl@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.52.4" +"@rollup/rollup-linux-riscv64-musl@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.53.3" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.52.4" +"@rollup/rollup-linux-s390x-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.53.3" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.52.4" +"@rollup/rollup-linux-x64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.53.3" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.52.4" +"@rollup/rollup-linux-x64-musl@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.53.3" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-openharmony-arm64@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-openharmony-arm64@npm:4.52.4" +"@rollup/rollup-openharmony-arm64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.53.3" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.52.4" +"@rollup/rollup-win32-arm64-msvc@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.53.3" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.52.4" +"@rollup/rollup-win32-ia32-msvc@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.53.3" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-gnu@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-win32-x64-gnu@npm:4.52.4" +"@rollup/rollup-win32-x64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.53.3" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.52.4": - version: 4.52.4 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.52.4" +"@rollup/rollup-win32-x64-msvc@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.53.3" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -4778,9 +4773,9 @@ __metadata: linkType: hard "@rushstack/eslint-patch@npm:^1.1.0": - version: 1.14.0 - resolution: "@rushstack/eslint-patch@npm:1.14.0" - checksum: cfa369e686b10b8888932836cf1e6e65c98ab5edd2bea07eae719070334b4010e3069ffa37e0579bbdd172893bb8d083165bad692eb14daa1734bdabcb9c29ad + version: 1.15.0 + resolution: "@rushstack/eslint-patch@npm:1.15.0" + checksum: 0b3f5951e66dabcaf5db4c1db789e1031701819caa69e09f8935cdd4aa0a41649936c2b2a5da795d79e5f56b8b6753b6e20bcfdf9343d4fb27a274a9566d7417 languageName: node linkType: hard @@ -4849,19 +4844,19 @@ __metadata: languageName: node linkType: hard -"@slack/types@npm:^2.17.0": - version: 2.17.0 - resolution: "@slack/types@npm:2.17.0" - checksum: 57cad4b3153589707fef50c7a231921364d10d8f4a3e4d342c718d4aa69f5b3541fee686e8b2d93d46dd4c9842adebe5d3bae6530e4ad8719c1ec5d46b0ec157 +"@slack/types@npm:^2.18.0": + version: 2.19.0 + resolution: "@slack/types@npm:2.19.0" + checksum: cdee616f4b457a15855ea7b45f5069f0bcf8a2754e2f32719c532a9c4bda73c5e70032f0edc153f6794b667ceb4bcaf539c792a98a2b55fbbb8106acca4b7366 languageName: node linkType: hard "@slack/web-api@npm:^7.8.0": - version: 7.11.0 - resolution: "@slack/web-api@npm:7.11.0" + version: 7.12.0 + resolution: "@slack/web-api@npm:7.12.0" dependencies: "@slack/logger": ^4.0.0 - "@slack/types": ^2.17.0 + "@slack/types": ^2.18.0 "@types/node": ">=18.0.0" "@types/retry": 0.12.0 axios: ^1.11.0 @@ -4872,96 +4867,97 @@ __metadata: p-queue: ^6 p-retry: ^4 retry: ^0.13.1 - checksum: 52c26b169111d15a6ef0701b947291ee97a8f4e795f9ee509ec90c56a4ef72f9776a3d35e54d7fa329b0479e7f193539e2e095b3ee0704a775bf66d960dfacf0 + checksum: 8807ecded3d83bfa60467fa120c0cc67150ffc1e37cdb2e43c09473e591681da7930d8b4cea51a578addffdcc487b519006c998a9029a5b372ec3a054f97ca8e languageName: node linkType: hard -"@smithy/abort-controller@npm:^4.2.3": - version: 4.2.3 - resolution: "@smithy/abort-controller@npm:4.2.3" +"@smithy/abort-controller@npm:^4.2.5": + version: 4.2.5 + resolution: "@smithy/abort-controller@npm:4.2.5" dependencies: - "@smithy/types": ^4.8.0 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 593d3b31a8f7e040071f448179fd6255ddc27fba43b0ba1777485aed3a47763e8746e7e529e124e3ad44c6ee00804fd744db020b435e1ac9137f0b2b608ca550 + checksum: f8f4142d02524e10fdca03258c46f1d41c6fd7ccf7c4716a6775fc9941c312abb230c3511e4f4cb178d8322e2752d8ea2fcaad1bed066ee6778f8db42cad59e2 languageName: node linkType: hard -"@smithy/config-resolver@npm:^4.3.2, @smithy/config-resolver@npm:^4.3.3": - version: 4.3.3 - resolution: "@smithy/config-resolver@npm:4.3.3" +"@smithy/config-resolver@npm:^4.4.3": + version: 4.4.3 + resolution: "@smithy/config-resolver@npm:4.4.3" dependencies: - "@smithy/node-config-provider": ^4.3.3 - "@smithy/types": ^4.8.0 + "@smithy/node-config-provider": ^4.3.5 + "@smithy/types": ^4.9.0 "@smithy/util-config-provider": ^4.2.0 - "@smithy/util-middleware": ^4.2.3 + "@smithy/util-endpoints": ^3.2.5 + "@smithy/util-middleware": ^4.2.5 tslib: ^2.6.2 - checksum: a8a9c706ff7f53ce0d38fb95ee88430a90f825e75afd24120dcdcf9d2f99305a54a88d72821555003ec5d406976148d5e54b0826860cc6bfbfb09bc5135a1ade + checksum: cd975065a2ab88499a5e2659a1375eef7798718ef26c503c328d7c10d48144b1eeeaa51008218380dcc2c411430fcdf2543f0049021a9a158f5b5a3f96955e38 languageName: node linkType: hard -"@smithy/core@npm:^3.16.1, @smithy/core@npm:^3.17.0": - version: 3.17.0 - resolution: "@smithy/core@npm:3.17.0" +"@smithy/core@npm:^3.18.5": + version: 3.18.5 + resolution: "@smithy/core@npm:3.18.5" dependencies: - "@smithy/middleware-serde": ^4.2.3 - "@smithy/protocol-http": ^5.3.3 - "@smithy/types": ^4.8.0 + "@smithy/middleware-serde": ^4.2.6 + "@smithy/protocol-http": ^5.3.5 + "@smithy/types": ^4.9.0 "@smithy/util-base64": ^4.3.0 "@smithy/util-body-length-browser": ^4.2.0 - "@smithy/util-middleware": ^4.2.3 - "@smithy/util-stream": ^4.5.3 + "@smithy/util-middleware": ^4.2.5 + "@smithy/util-stream": ^4.5.6 "@smithy/util-utf8": ^4.2.0 "@smithy/uuid": ^1.1.0 tslib: ^2.6.2 - checksum: 331667c4214862c709cb97cfab4e44c9213cd54e3aa7a47b437559515580c45e85a27ef1b226bd2744247bbf474c73fb20b7a22455c85eb7339f3b4addcc8d33 + checksum: 82c33810b17bda666083d3968fbac9fb14c0d4d7e648afd3cecc3570835ba1fa0fba2131777ef59922980d9b66d74daf8c71915a83306751ed2fd7b056ffa749 languageName: node linkType: hard -"@smithy/credential-provider-imds@npm:^4.2.2, @smithy/credential-provider-imds@npm:^4.2.3": - version: 4.2.3 - resolution: "@smithy/credential-provider-imds@npm:4.2.3" +"@smithy/credential-provider-imds@npm:^4.2.5": + version: 4.2.5 + resolution: "@smithy/credential-provider-imds@npm:4.2.5" dependencies: - "@smithy/node-config-provider": ^4.3.3 - "@smithy/property-provider": ^4.2.3 - "@smithy/types": ^4.8.0 - "@smithy/url-parser": ^4.2.3 + "@smithy/node-config-provider": ^4.3.5 + "@smithy/property-provider": ^4.2.5 + "@smithy/types": ^4.9.0 + "@smithy/url-parser": ^4.2.5 tslib: ^2.6.2 - checksum: bcc92e9e4618caed80b80a653b8b332a5e1411c51c6caaf23adbfc76843accc207e24ebba9027ebaeb8c09e3ac7d8508ff1f9369e4ad3912e1abb0f9d689ad4b + checksum: a0ee500ab692a357bccbc10913b6af602e753940a34157a1086d05ce605287e8d66396048c49d453fa80a789c4cff64b6fe92a4baabbc75a666b531aa6500e82 languageName: node linkType: hard -"@smithy/fetch-http-handler@npm:^5.3.3, @smithy/fetch-http-handler@npm:^5.3.4": - version: 5.3.4 - resolution: "@smithy/fetch-http-handler@npm:5.3.4" +"@smithy/fetch-http-handler@npm:^5.3.6": + version: 5.3.6 + resolution: "@smithy/fetch-http-handler@npm:5.3.6" dependencies: - "@smithy/protocol-http": ^5.3.3 - "@smithy/querystring-builder": ^4.2.3 - "@smithy/types": ^4.8.0 + "@smithy/protocol-http": ^5.3.5 + "@smithy/querystring-builder": ^4.2.5 + "@smithy/types": ^4.9.0 "@smithy/util-base64": ^4.3.0 tslib: ^2.6.2 - checksum: 98aad48e545fb49f0fdb7562a267a4fc963fa6ef250b5dbf49da2f9aedfdbf0e43c36584a2ff0e73ddf28e504ab0d7451336b0316029cf935aa00d647ba244a4 + checksum: 4b98efb0d8633d4f9da68e7fc06a02689d116142f8dbc1711ef7e60863a81d57ff3b869684db7f0c479214be8d949dd4c94fa7a9f5b62af1891fe2d661ff9667 languageName: node linkType: hard -"@smithy/hash-node@npm:^4.2.2": - version: 4.2.3 - resolution: "@smithy/hash-node@npm:4.2.3" +"@smithy/hash-node@npm:^4.2.5": + version: 4.2.5 + resolution: "@smithy/hash-node@npm:4.2.5" dependencies: - "@smithy/types": ^4.8.0 + "@smithy/types": ^4.9.0 "@smithy/util-buffer-from": ^4.2.0 "@smithy/util-utf8": ^4.2.0 tslib: ^2.6.2 - checksum: b3363f2ce20568b6d0694ed2df5ef37c60fa12947afe4f815029a0e37162bafba9482017e0aa14a51a75131d09adc531b2dd9c42f8f04e28b02752b66970b061 + checksum: 79a75122f2e9bd7724c1c03b34c66bbd7aad8b3dc4cf07b3052a9aaeedde78f2705e8f88db368928cc82c499f3e0a540483f1c428230617c089a552420f87063 languageName: node linkType: hard -"@smithy/invalid-dependency@npm:^4.2.2": - version: 4.2.3 - resolution: "@smithy/invalid-dependency@npm:4.2.3" +"@smithy/invalid-dependency@npm:^4.2.5": + version: 4.2.5 + resolution: "@smithy/invalid-dependency@npm:4.2.5" dependencies: - "@smithy/types": ^4.8.0 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: c71729976219bd784f1bf565e3dd813447259e9a75e8367b98c33c71ad5f1220f0419be37a0e37dbf5ca4a0e03beb128329f32e9963389e4c9e31a98a358976f + checksum: fabdefc6d51dc04bcd73d8da865aa0a7192fd4b295da5005f0951884997304a2985af1a8f956d29d4b5e765027a3a4605c383329041cfbbfc0baf493dfdc1799 languageName: node linkType: hard @@ -4983,204 +4979,204 @@ __metadata: languageName: node linkType: hard -"@smithy/middleware-content-length@npm:^4.2.2": - version: 4.2.3 - resolution: "@smithy/middleware-content-length@npm:4.2.3" +"@smithy/middleware-content-length@npm:^4.2.5": + version: 4.2.5 + resolution: "@smithy/middleware-content-length@npm:4.2.5" dependencies: - "@smithy/protocol-http": ^5.3.3 - "@smithy/types": ^4.8.0 + "@smithy/protocol-http": ^5.3.5 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 70491b7688f1d5f066872799db786447f47e727c980c40886205b121508030719c76076118ea823313c6d432baa49203b9ff0cf7fd7dfbed68398a5f1fcf0320 + checksum: c339fdf8cef2e24e60d931ce8cae795b048643a101859034db817c8978b5e09b9ee1e4452c320b46fbd6cb4f3de704e5d474e8c86053dac06ee6c874aa70afae languageName: node linkType: hard -"@smithy/middleware-endpoint@npm:^4.3.3, @smithy/middleware-endpoint@npm:^4.3.4": - version: 4.3.4 - resolution: "@smithy/middleware-endpoint@npm:4.3.4" +"@smithy/middleware-endpoint@npm:^4.3.12": + version: 4.3.12 + resolution: "@smithy/middleware-endpoint@npm:4.3.12" dependencies: - "@smithy/core": ^3.17.0 - "@smithy/middleware-serde": ^4.2.3 - "@smithy/node-config-provider": ^4.3.3 - "@smithy/shared-ini-file-loader": ^4.3.3 - "@smithy/types": ^4.8.0 - "@smithy/url-parser": ^4.2.3 - "@smithy/util-middleware": ^4.2.3 + "@smithy/core": ^3.18.5 + "@smithy/middleware-serde": ^4.2.6 + "@smithy/node-config-provider": ^4.3.5 + "@smithy/shared-ini-file-loader": ^4.4.0 + "@smithy/types": ^4.9.0 + "@smithy/url-parser": ^4.2.5 + "@smithy/util-middleware": ^4.2.5 tslib: ^2.6.2 - checksum: 2d574f081eccedb6b6956a466caeb3f5438d9442f437c13715298c7b6dcec668ec23e7566bacab4ac72fe08c06f844e112764c725479e0dd0e2cc77dc087225c + checksum: 16cbb1d09ab9662d1443906b9796241b314b128367d713cfd07ea15c19ff1eb57bb2d3b6aa3dfbbdafbba00b3b6ff44d177d994d297766153ea430bea1bc619b languageName: node linkType: hard -"@smithy/middleware-retry@npm:^4.4.3": - version: 4.4.4 - resolution: "@smithy/middleware-retry@npm:4.4.4" - dependencies: - "@smithy/node-config-provider": ^4.3.3 - "@smithy/protocol-http": ^5.3.3 - "@smithy/service-error-classification": ^4.2.3 - "@smithy/smithy-client": ^4.9.0 - "@smithy/types": ^4.8.0 - "@smithy/util-middleware": ^4.2.3 - "@smithy/util-retry": ^4.2.3 +"@smithy/middleware-retry@npm:^4.4.12": + version: 4.4.12 + resolution: "@smithy/middleware-retry@npm:4.4.12" + dependencies: + "@smithy/node-config-provider": ^4.3.5 + "@smithy/protocol-http": ^5.3.5 + "@smithy/service-error-classification": ^4.2.5 + "@smithy/smithy-client": ^4.9.8 + "@smithy/types": ^4.9.0 + "@smithy/util-middleware": ^4.2.5 + "@smithy/util-retry": ^4.2.5 "@smithy/uuid": ^1.1.0 tslib: ^2.6.2 - checksum: 8d4e547af7966fff603a9fe12c88bc05ca1db89a49b8c809fd9cedcd401119ba85b379cbec01560bb83d662abf1de42ff21322527bc1cd5f95b0e23abbf849db + checksum: 6a92f18eaa7ae85f64fe114ce63067d96e071c03fc420df9845619fa4aeb3b473f2d7553563c0e56a47e58314e26d8b2e2f38a1fbb76d43d221c9d188384d063 languageName: node linkType: hard -"@smithy/middleware-serde@npm:^4.2.2, @smithy/middleware-serde@npm:^4.2.3": - version: 4.2.3 - resolution: "@smithy/middleware-serde@npm:4.2.3" +"@smithy/middleware-serde@npm:^4.2.6": + version: 4.2.6 + resolution: "@smithy/middleware-serde@npm:4.2.6" dependencies: - "@smithy/protocol-http": ^5.3.3 - "@smithy/types": ^4.8.0 + "@smithy/protocol-http": ^5.3.5 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: c21e26a9a0b4e0e3e7b35522830aed272b244d5700430f6fc532c7b297a07188b5eb6ac6381fb543ee42a6143ca4f524bac2225f0e23bcd10162800c81f72a79 + checksum: 30791fb3bb4e34562c9f44268c92c162a04cc8c5fdf3069941eeb566eb141ad72eb9745d2b6363bf7ce3b2b2457b6714de26d2bdc3c83e74af25379ee597a15c languageName: node linkType: hard -"@smithy/middleware-stack@npm:^4.2.2, @smithy/middleware-stack@npm:^4.2.3": - version: 4.2.3 - resolution: "@smithy/middleware-stack@npm:4.2.3" +"@smithy/middleware-stack@npm:^4.2.5": + version: 4.2.5 + resolution: "@smithy/middleware-stack@npm:4.2.5" dependencies: - "@smithy/types": ^4.8.0 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: a5ddd0ff95c8ae2af052e59a78916299c1ecb963e48a8812c960e43c6f93ad7e3956948d00b14a5e902850d172a6547136dd896e9999fd0a8a173610c74cd35f + checksum: abb4053e859f6acf9cdeafc0b3de54d7aa6f5fcdf655c4bbe3840c7bc733ddd3009d0caff7ff86b2c3260f07bd2b7f042c49ab5537131aed68e22a5be2199cfe languageName: node linkType: hard -"@smithy/node-config-provider@npm:^4.3.2, @smithy/node-config-provider@npm:^4.3.3": - version: 4.3.3 - resolution: "@smithy/node-config-provider@npm:4.3.3" +"@smithy/node-config-provider@npm:^4.3.5": + version: 4.3.5 + resolution: "@smithy/node-config-provider@npm:4.3.5" dependencies: - "@smithy/property-provider": ^4.2.3 - "@smithy/shared-ini-file-loader": ^4.3.3 - "@smithy/types": ^4.8.0 + "@smithy/property-provider": ^4.2.5 + "@smithy/shared-ini-file-loader": ^4.4.0 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: d7bd751aa21162d06b927429ec017f1c705838f902e5ef7d54a2ebc70a161ded4202b31ffa9b4c5c20487b8d77e347bac1fd41907f53181c555655b5c6226dba + checksum: 18f9e043502009dee693f3ad5f409aa7c1f20d59354fa37e977c94c38164f36144c4942ef0e0b94b02370047956d0305566167f3b2491919402f87f38b21ba8b languageName: node linkType: hard -"@smithy/node-http-handler@npm:^4.4.1, @smithy/node-http-handler@npm:^4.4.2": - version: 4.4.2 - resolution: "@smithy/node-http-handler@npm:4.4.2" +"@smithy/node-http-handler@npm:^4.4.5": + version: 4.4.5 + resolution: "@smithy/node-http-handler@npm:4.4.5" dependencies: - "@smithy/abort-controller": ^4.2.3 - "@smithy/protocol-http": ^5.3.3 - "@smithy/querystring-builder": ^4.2.3 - "@smithy/types": ^4.8.0 + "@smithy/abort-controller": ^4.2.5 + "@smithy/protocol-http": ^5.3.5 + "@smithy/querystring-builder": ^4.2.5 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: e28cdbcab3796666ad5b297de2505876536ec1c3240baa86aeee7f74fe56b65f234844af1ba928ae47564e345a41140c1571f9a804d4fd4c7e8679505a309cc1 + checksum: cfd085f186727c5ad8c2ba55cb21af489150b19d2feb1691adc1025bcdcd72c7bc293302f2b05127e6ae7235d71578418d2dd3ea5f7471eaa11af1c0c8247022 languageName: node linkType: hard -"@smithy/property-provider@npm:^4.2.2, @smithy/property-provider@npm:^4.2.3": - version: 4.2.3 - resolution: "@smithy/property-provider@npm:4.2.3" +"@smithy/property-provider@npm:^4.2.5": + version: 4.2.5 + resolution: "@smithy/property-provider@npm:4.2.5" dependencies: - "@smithy/types": ^4.8.0 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 6f1978e11cee72e4bb7211925520f347f2d7262b96a83638e316342e6443aa572ac16ceac6d63a08cb49a67b4680c9740590f5e52c042dd6c5ad90db1ac8d575 + checksum: c2892755d890b9f7402955a67eb89eb4f020e925e727056b06fafd1470a61462b01c9ebdc3c738eb7d4a8a609e55ac5dc76711cf9681ea66133d29a8315b93ac languageName: node linkType: hard -"@smithy/protocol-http@npm:^5.3.2, @smithy/protocol-http@npm:^5.3.3": - version: 5.3.3 - resolution: "@smithy/protocol-http@npm:5.3.3" +"@smithy/protocol-http@npm:^5.3.5": + version: 5.3.5 + resolution: "@smithy/protocol-http@npm:5.3.5" dependencies: - "@smithy/types": ^4.8.0 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 58d135cf47e34779088ffaaaeda88779f686852a9d9d04361966604f1f36da3dd3f855c23fc33fb2d92c99d0fb22109a99ae58d271f8878e205850fea5480c69 + checksum: 1fcd7b729d18e185a5172aeda213b78281f735353651dfe368770511a57a99a67f79e82e0ef84800a24477623ed3cd79828f1d63551b1bf24ddfb3aa782ddc8a languageName: node linkType: hard -"@smithy/querystring-builder@npm:^4.2.3": - version: 4.2.3 - resolution: "@smithy/querystring-builder@npm:4.2.3" +"@smithy/querystring-builder@npm:^4.2.5": + version: 4.2.5 + resolution: "@smithy/querystring-builder@npm:4.2.5" dependencies: - "@smithy/types": ^4.8.0 + "@smithy/types": ^4.9.0 "@smithy/util-uri-escape": ^4.2.0 tslib: ^2.6.2 - checksum: 0a8d6c533ac302d4ddcb7f620f7b71d0207a2681371cad1426c494c8282b64573716733562f7af319469f0aa09b242a1be9a6136dee266bf62aa087d045d9bd9 + checksum: 3d3203946b3776a9ece67c10f3c92bafe54163fccc4125451e22eb4deb7bb42ab746cfe48ccc86749d8d550d7e388931129afbbe9a1c912fb5201d0e16b1f59d languageName: node linkType: hard -"@smithy/querystring-parser@npm:^4.2.3": - version: 4.2.3 - resolution: "@smithy/querystring-parser@npm:4.2.3" +"@smithy/querystring-parser@npm:^4.2.5": + version: 4.2.5 + resolution: "@smithy/querystring-parser@npm:4.2.5" dependencies: - "@smithy/types": ^4.8.0 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: f514568f2caf4944c012c6a244e3340fdb478b7591d2560dbefbbfc79d248a7690e9b645185a81d2723b6e8520e82bd8b0b1105805959f450d78894bcfab29ef + checksum: 006af8cb08cd543d5a84624e713e14ba4f3f56b3a45592dc0ae093f7e1bcbea11f6c99158e8834db01d72d713220998ce902a18ccc290bf83fea78696e033039 languageName: node linkType: hard -"@smithy/service-error-classification@npm:^4.2.3": - version: 4.2.3 - resolution: "@smithy/service-error-classification@npm:4.2.3" +"@smithy/service-error-classification@npm:^4.2.5": + version: 4.2.5 + resolution: "@smithy/service-error-classification@npm:4.2.5" dependencies: - "@smithy/types": ^4.8.0 - checksum: d3e831bbf946049edef918f8051d5c284581ef985e29c8154ee5291775c99ebb78b57790b071582c77b587370bee92293f784915675df5baca12f2194b4bb6bc + "@smithy/types": ^4.9.0 + checksum: b225382e6af81807623694f3cdf5067569ba24fabe1b1fbf0baa5d65ff8dd4071f2ac526188f24de87588b2462f0902db405bd9bf161c5c739f1426e4ae1e199 languageName: node linkType: hard -"@smithy/shared-ini-file-loader@npm:^4.3.2, @smithy/shared-ini-file-loader@npm:^4.3.3": - version: 4.3.3 - resolution: "@smithy/shared-ini-file-loader@npm:4.3.3" +"@smithy/shared-ini-file-loader@npm:^4.4.0": + version: 4.4.0 + resolution: "@smithy/shared-ini-file-loader@npm:4.4.0" dependencies: - "@smithy/types": ^4.8.0 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 8baddfbb4929b16a8a16b17f05add104b7e637830f07dcec7eb2828f3b00e3cc64ee9a67d024ff74513fe2d6e3b93465561405276f50155d09034869ba0a85cc + checksum: ecd913fd8090a2c06eec81e4f79ea28066cb046fc4769eea20dcc0e781b0636e7542c53eb189a1987d5d0337b8a950b7de49c10351febcb712b6be58f979e0ef languageName: node linkType: hard -"@smithy/signature-v4@npm:^5.3.2": - version: 5.3.3 - resolution: "@smithy/signature-v4@npm:5.3.3" +"@smithy/signature-v4@npm:^5.3.5": + version: 5.3.5 + resolution: "@smithy/signature-v4@npm:5.3.5" dependencies: "@smithy/is-array-buffer": ^4.2.0 - "@smithy/protocol-http": ^5.3.3 - "@smithy/types": ^4.8.0 + "@smithy/protocol-http": ^5.3.5 + "@smithy/types": ^4.9.0 "@smithy/util-hex-encoding": ^4.2.0 - "@smithy/util-middleware": ^4.2.3 + "@smithy/util-middleware": ^4.2.5 "@smithy/util-uri-escape": ^4.2.0 "@smithy/util-utf8": ^4.2.0 tslib: ^2.6.2 - checksum: 81092dc68caf376d8a66098227e4cf9670f78ae31e5ffe751abb44804d0de9abcdc9f8341e05f84e63e90dac0c59fde64b0eebfa99ef2551c9f535d9b1f7f215 + checksum: 617b2ee5a20a581b14285b48d36c99e9cf939e7c347d287b142609045306082886abcfaba3dbe60fff9fdd91593acb31403107ed135eb54847e788d615f67d54 languageName: node linkType: hard -"@smithy/smithy-client@npm:^4.8.1, @smithy/smithy-client@npm:^4.9.0": - version: 4.9.0 - resolution: "@smithy/smithy-client@npm:4.9.0" - dependencies: - "@smithy/core": ^3.17.0 - "@smithy/middleware-endpoint": ^4.3.4 - "@smithy/middleware-stack": ^4.2.3 - "@smithy/protocol-http": ^5.3.3 - "@smithy/types": ^4.8.0 - "@smithy/util-stream": ^4.5.3 +"@smithy/smithy-client@npm:^4.9.8": + version: 4.9.8 + resolution: "@smithy/smithy-client@npm:4.9.8" + dependencies: + "@smithy/core": ^3.18.5 + "@smithy/middleware-endpoint": ^4.3.12 + "@smithy/middleware-stack": ^4.2.5 + "@smithy/protocol-http": ^5.3.5 + "@smithy/types": ^4.9.0 + "@smithy/util-stream": ^4.5.6 tslib: ^2.6.2 - checksum: eea3bb436a234fc3bd5843d91754ff7f2de9530d051d941763111568288c5ce99abc34a118b78c9d8a4c942799916be0779234df26eb3b61d915605395485d8b + checksum: 830e09757458fcdfbe6094f2bb93c7ebaeebc316cdf32fd4f92420fbd93a8524c4cefd64f236fc112ee17370a415dadfd4aac87d8167ac7d5beb779c74513510 languageName: node linkType: hard -"@smithy/types@npm:^4.7.1, @smithy/types@npm:^4.8.0": - version: 4.8.0 - resolution: "@smithy/types@npm:4.8.0" +"@smithy/types@npm:^4.9.0": + version: 4.9.0 + resolution: "@smithy/types@npm:4.9.0" dependencies: tslib: ^2.6.2 - checksum: 61447f6799bc2a536e92be47aa7744c54a41d041d26ee8004e1dfd8bb91dda3d27e47e258d88f9da7a60b7ab730d0991495d028fef649f22c5208729d37ebd35 + checksum: 7a9444d882d6c60deca5009e5b7a3991192c77349d42d88a4eb2a9ff2224bb046e2aa42b239e22455770cf46449184657b8ae180f7fd6c5161c05f7ec6b191c1 languageName: node linkType: hard -"@smithy/url-parser@npm:^4.2.2, @smithy/url-parser@npm:^4.2.3": - version: 4.2.3 - resolution: "@smithy/url-parser@npm:4.2.3" +"@smithy/url-parser@npm:^4.2.5": + version: 4.2.5 + resolution: "@smithy/url-parser@npm:4.2.5" dependencies: - "@smithy/querystring-parser": ^4.2.3 - "@smithy/types": ^4.8.0 + "@smithy/querystring-parser": ^4.2.5 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 1a3b64fb811b26bc6e1d9a6d9401b50a25f63dced60faf3bf1f000b2a217e9e3dedc321aa05fc40151bc947502d02d0c6cfa9a8ba3d2daab0706eab8a8ac798e + checksum: 8952f1183a6ea748ca9ddeffd5048e0ba0d6a0fcfdef2eefdf1c9ce69ef7a8b48b0872b2eba3310eb704443b322694aceea2ca83a5ef1b7fecc9159a52b1e23f languageName: node linkType: hard @@ -5242,41 +5238,41 @@ __metadata: languageName: node linkType: hard -"@smithy/util-defaults-mode-browser@npm:^4.3.2": - version: 4.3.3 - resolution: "@smithy/util-defaults-mode-browser@npm:4.3.3" +"@smithy/util-defaults-mode-browser@npm:^4.3.11": + version: 4.3.11 + resolution: "@smithy/util-defaults-mode-browser@npm:4.3.11" dependencies: - "@smithy/property-provider": ^4.2.3 - "@smithy/smithy-client": ^4.9.0 - "@smithy/types": ^4.8.0 + "@smithy/property-provider": ^4.2.5 + "@smithy/smithy-client": ^4.9.8 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 5c6cc5926cec2f8fdd0260a8a5a77a871e3c4cbf5af779e16289aee92e10a02af167f4034e421a2d9c9c2b3b141bf12bfea6fe2d6ce07f7d981dc687872a32be + checksum: 296347f54034a5248ae4f4f3055ab5e72f7feb5f51e536854a3b9e1c63d1f93822524c38d60a3629ea220bb01a80ca08c0e702e7278a7d3d7eb88467dad1852e languageName: node linkType: hard -"@smithy/util-defaults-mode-node@npm:^4.2.3": - version: 4.2.4 - resolution: "@smithy/util-defaults-mode-node@npm:4.2.4" - dependencies: - "@smithy/config-resolver": ^4.3.3 - "@smithy/credential-provider-imds": ^4.2.3 - "@smithy/node-config-provider": ^4.3.3 - "@smithy/property-provider": ^4.2.3 - "@smithy/smithy-client": ^4.9.0 - "@smithy/types": ^4.8.0 +"@smithy/util-defaults-mode-node@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/util-defaults-mode-node@npm:4.2.14" + dependencies: + "@smithy/config-resolver": ^4.4.3 + "@smithy/credential-provider-imds": ^4.2.5 + "@smithy/node-config-provider": ^4.3.5 + "@smithy/property-provider": ^4.2.5 + "@smithy/smithy-client": ^4.9.8 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 5fdd516b820ed7704cbcf6d07d1e9b14e60790f639a009c338e9b14b5b849ed9b65367ce6eb08cefcee12d18f3900bf5bb4425858cbb3f3e3990907367f01fc5 + checksum: 797a2a175609f2c8c29c7db8eabffa2256ba836a47651153bc8ff60bc7c012b2f4b2fd72143c26b430f0af179bde51c10d31c3a59d177506ef42e0b1df8c76d3 languageName: node linkType: hard -"@smithy/util-endpoints@npm:^3.2.2": - version: 3.2.3 - resolution: "@smithy/util-endpoints@npm:3.2.3" +"@smithy/util-endpoints@npm:^3.2.5": + version: 3.2.5 + resolution: "@smithy/util-endpoints@npm:3.2.5" dependencies: - "@smithy/node-config-provider": ^4.3.3 - "@smithy/types": ^4.8.0 + "@smithy/node-config-provider": ^4.3.5 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: e86bb072e4d8234f787fdb721249b30ecfc268475d326591b019eba1270f83bf5b20a7fc320be6ab6ebcb7c3764c3a424e2f30e7921025a5efee175c69c1d0fd + checksum: 16fbfe26aa1c3f3bb8edfb24e6127707a1bf94161c20fd8af98a34b910f1679d4a73b6659c8c60f846281ebf98d2bfd3e2dfa9556eb36d02ec6c6b80a2a56235 languageName: node linkType: hard @@ -5289,40 +5285,40 @@ __metadata: languageName: node linkType: hard -"@smithy/util-middleware@npm:^4.2.2, @smithy/util-middleware@npm:^4.2.3": - version: 4.2.3 - resolution: "@smithy/util-middleware@npm:4.2.3" +"@smithy/util-middleware@npm:^4.2.5": + version: 4.2.5 + resolution: "@smithy/util-middleware@npm:4.2.5" dependencies: - "@smithy/types": ^4.8.0 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 80b18868bcc684fe29958870a2f48f0d6062d539639125e35eb37dda43b0143fdf944c27fe7c623ec38a5abccaac971cee8aadbc993d5af4a283324281f19622 + checksum: 4c024978a399a4c0d4856823717555ca4b2c41b1bd366d63094e19a92212001f66c10b313b6fafeb4ce7f7aad4865093ad6b4544f44fba9ae976e866a2d40f8e languageName: node linkType: hard -"@smithy/util-retry@npm:^4.2.2, @smithy/util-retry@npm:^4.2.3": - version: 4.2.3 - resolution: "@smithy/util-retry@npm:4.2.3" +"@smithy/util-retry@npm:^4.2.5": + version: 4.2.5 + resolution: "@smithy/util-retry@npm:4.2.5" dependencies: - "@smithy/service-error-classification": ^4.2.3 - "@smithy/types": ^4.8.0 + "@smithy/service-error-classification": ^4.2.5 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 51f6a20b1e31f44c56f0b87786a7a3a4f39c63119820220f9f79529fe2ebf67958463246ca0e3e07a018587a8a9ccb61587fabf399574a05b65d769fbc9ce033 + checksum: b9e105b56776ef40496c76b1e925df5409c0e2473099da34cb53db6441f1e097547b5b2cd1231be14d97532bd378de470b4b8c229879e9a8ed34aae6aef8fdb3 languageName: node linkType: hard -"@smithy/util-stream@npm:^4.5.2, @smithy/util-stream@npm:^4.5.3": - version: 4.5.3 - resolution: "@smithy/util-stream@npm:4.5.3" +"@smithy/util-stream@npm:^4.5.6": + version: 4.5.6 + resolution: "@smithy/util-stream@npm:4.5.6" dependencies: - "@smithy/fetch-http-handler": ^5.3.4 - "@smithy/node-http-handler": ^4.4.2 - "@smithy/types": ^4.8.0 + "@smithy/fetch-http-handler": ^5.3.6 + "@smithy/node-http-handler": ^4.4.5 + "@smithy/types": ^4.9.0 "@smithy/util-base64": ^4.3.0 "@smithy/util-buffer-from": ^4.2.0 "@smithy/util-hex-encoding": ^4.2.0 "@smithy/util-utf8": ^4.2.0 tslib: ^2.6.2 - checksum: b7b680548b69473334535f0a951ee3e91e5d7fa6aa78d54bf9c3853fd07c74f54ae742ef1d818f6383044c01d3be289f4c2aa71a9753edcbccf90531b8123955 + checksum: ebe98f004e13f6af4e14a30137380626bd1559220d98d22a8eb6871ac034cd820824ef31a1f4d7b9165df8d6fc960d54924c0c3024a0fd5efee17bfb3723fa0b languageName: node linkType: hard @@ -5355,14 +5351,14 @@ __metadata: languageName: node linkType: hard -"@smithy/util-waiter@npm:^4.2.2": - version: 4.2.3 - resolution: "@smithy/util-waiter@npm:4.2.3" +"@smithy/util-waiter@npm:^4.2.5": + version: 4.2.5 + resolution: "@smithy/util-waiter@npm:4.2.5" dependencies: - "@smithy/abort-controller": ^4.2.3 - "@smithy/types": ^4.8.0 + "@smithy/abort-controller": ^4.2.5 + "@smithy/types": ^4.9.0 tslib: ^2.6.2 - checksum: 23879787869e24f9df9a1bab18fc5ec09379c914ba98523f5ac66b1a2a74ad319a4ca0024211a107882d52edee87411069fe3933217b5298cfec7ba229e2a7e2 + checksum: b881008963e99caa9aba2839b387a747f6b2169e21baebaaa05ecf837be6a8596b723fcf69c0f15043a01badee94a2dab803711059bf5a1e7886b0af894dbb0e languageName: node linkType: hard @@ -5621,9 +5617,9 @@ __metadata: linkType: hard "@tsconfig/node10@npm:^1.0.7": - version: 1.0.11 - resolution: "@tsconfig/node10@npm:1.0.11" - checksum: 51fe47d55fe1b80ec35e6e5ed30a13665fd3a531945350aa74a14a1e82875fb60b350c2f2a5e72a64831b1b6bc02acb6760c30b3738b54954ec2dea82db7a267 + version: 1.0.12 + resolution: "@tsconfig/node10@npm:1.0.12" + checksum: 27e2f989dbb20f773aa121b609a5361a473b7047ff286fce7c851e61f5eec0c74f0bdb38d5bd69c8a06f17e60e9530188f2219b1cbeabeac91f0a5fd348eac2a languageName: node linkType: hard @@ -5723,11 +5719,12 @@ __metadata: linkType: hard "@types/chai@npm:^5.2.2": - version: 5.2.2 - resolution: "@types/chai@npm:5.2.2" + version: 5.2.3 + resolution: "@types/chai@npm:5.2.3" dependencies: "@types/deep-eql": "*" - checksum: 386887bd55ba684572cececd833ed91aba6cce2edd8cc1d8cefa78800b3a74db6dbf5c5c41af041d1d1f3ce672ea30b45c9520f948cdc75431eb7df3fbba8405 + assertion-error: ^2.0.1 + checksum: eb4c2da9ec38b474a983f39bfb5ec4fbcceb5e5d76d184094d2cbc4c41357973eb5769c8972cedac665a233251b0ed754f1e338fcf408d381968af85cdecc596 languageName: node linkType: hard @@ -5760,11 +5757,11 @@ __metadata: linkType: hard "@types/cookie-parser@npm:^1.4.3": - version: 1.4.9 - resolution: "@types/cookie-parser@npm:1.4.9" + version: 1.4.10 + resolution: "@types/cookie-parser@npm:1.4.10" peerDependencies: "@types/express": "*" - checksum: 6192a4899b5412a4c3be0f47158321aef73a4cd7e7a4f7b2a37e2e1045f11a21209681cb1bc5335f250ee2a6ce64d8a3fefb851181a98e6415d3716ef9ed1f62 + checksum: 1f37b5a4115dbfd4b7bbea2d874fbf9495eca8c3e8c87fa7e38c50f9fff66222377c911cfdc7a1ea08855e822919c5534ebbcc4bf25b596bc6f7270e403483d9 languageName: node linkType: hard @@ -5966,25 +5963,25 @@ __metadata: linkType: hard "@types/express@npm:*, @types/express@npm:^5.0.0": - version: 5.0.3 - resolution: "@types/express@npm:5.0.3" + version: 5.0.5 + resolution: "@types/express@npm:5.0.5" dependencies: "@types/body-parser": "*" "@types/express-serve-static-core": ^5.0.0 - "@types/serve-static": "*" - checksum: bb6f10c14c8e3cce07f79ee172688aa9592852abd7577b663cd0c2054307f172c2b2b36468c918fed0d4ac359b99695807b384b3da6157dfa79acbac2226b59b + "@types/serve-static": ^1 + checksum: 9e72410286fbc80bea8a57d1b374c25235f6019dabf8af67e5b72c2f9be548f36ccc09d048fc88172731450e80f06018f90c17e05beb5afc4dbdaf5f7200dbb3 languageName: node linkType: hard "@types/express@npm:^4.17.0, @types/express@npm:^4.17.13": - version: 4.17.23 - resolution: "@types/express@npm:4.17.23" + version: 4.17.25 + resolution: "@types/express@npm:4.17.25" dependencies: "@types/body-parser": "*" "@types/express-serve-static-core": ^4.17.33 "@types/qs": "*" - "@types/serve-static": "*" - checksum: f1a020c6ad6dc0169a3277199605d60649d463a72c920673e0f230ab1e76f2d3aa23daabd25ef8e62eda314e26b879306c58ec806e669e1e40ca37a4b73a44b0 + "@types/serve-static": ^1 + checksum: 285d16008489d37b2be03e2e050bcf201d5d6ed9278ca13619d9029efd2055b192b2445f769116f716cfcf53d9d799a03f4e76199af9cea0ea3dee3d88595931 languageName: node linkType: hard @@ -6035,11 +6032,11 @@ __metadata: linkType: hard "@types/http-proxy@npm:^1.17.8": - version: 1.17.16 - resolution: "@types/http-proxy@npm:1.17.16" + version: 1.17.17 + resolution: "@types/http-proxy@npm:1.17.17" dependencies: "@types/node": "*" - checksum: f5ab4afe7e3feba9d87bdddbf44e03d9a836bd2cdab679a794badbff7c4bfb6bebf46bfe22d9964eb1820e1349f2ff7807cccb20fd27cb17f03db849289e5892 + checksum: 7231460dc06c109447b21c125a60662872b9c2e902efd12c47902b8ad75caded19678fa3115f6b9ce06b94d2f46d697be572e848c52da558f4f1ee88ff18a2e0 languageName: node linkType: hard @@ -6111,9 +6108,9 @@ __metadata: linkType: hard "@types/lodash@npm:*": - version: 4.17.20 - resolution: "@types/lodash@npm:4.17.20" - checksum: dc7bb4653514dd91117a4c4cec2c37e2b5a163d7643445e4757d76a360fabe064422ec7a42dde7450c5e7e0e7e678d5e6eae6d2a919abcddf581d81e63e63839 + version: 4.17.21 + resolution: "@types/lodash@npm:4.17.21" + checksum: e09e3eaf29b18b6c8e130fcbafd8e3c4ecc2110f35255079245e7d1bd310a5a8f93e4e70266533dce672f253bba899c721bc6870097e0a8c5448e0b628136d39 languageName: node linkType: hard @@ -6166,11 +6163,11 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:>=18.0.0": - version: 24.8.1 - resolution: "@types/node@npm:24.8.1" + version: 24.10.1 + resolution: "@types/node@npm:24.10.1" dependencies: - undici-types: ~7.14.0 - checksum: 55fca8f900a017c207e4e413956f3fdd17178c3d5ffc8fd4bb7b0b5136865ca06076b1335e1e5e6f3269cace491dc1f4ecc1ab4410ff4acf25c9087585560c86 + undici-types: ~7.16.0 + checksum: c2f370ae7a97c04991e0eee6b57e588a2abef0814a5f6e41fda5a9200cf02ae6654fad51c8372ee203ae4134bab80f5bf441c586f7f50e0fda3ba422f35eb3c0 languageName: node linkType: hard @@ -6196,21 +6193,21 @@ __metadata: linkType: hard "@types/node@npm:^20.0.0": - version: 20.19.22 - resolution: "@types/node@npm:20.19.22" + version: 20.19.25 + resolution: "@types/node@npm:20.19.25" dependencies: undici-types: ~6.21.0 - checksum: f6ac4f9e407eb3594ba623e9c1c85de109b1b8e0fdd52fbed40f9c728e5fd0df053e873458c86eed2aa2fb41f36219b631e757b848e4cb826a2e3657df55ff02 + checksum: f6d854995d173e9d85c7ffd4ca69ee645daac8c0a150fc3ed2f7b21dfc7ece34e44063213c4e9b11de56fe7de807d59c1bb717fca8af8590b40bfb9d9068e629 languageName: node linkType: hard "@types/nodemailer@npm:^6.4.0": - version: 6.4.20 - resolution: "@types/nodemailer@npm:6.4.20" + version: 6.4.21 + resolution: "@types/nodemailer@npm:6.4.21" dependencies: "@aws-sdk/client-ses": ^3.731.1 "@types/node": "*" - checksum: 691b1f8344641df0dbb7444efb65adf84794b4b3ff4797c85973ff463fc035ecd13c15e7946129588df5a653bc0f66c93dd2cada0d3d4450604dcb1fbe9bc54b + checksum: 1c6e115f0db7cfd5e5690f0b5c2d3695037338b18b2a91d18a0a425cbf5741fe5399b2fa7a400c5a4493a1885afabdf9a588355562803d287f3c2eacc84a0af9 languageName: node linkType: hard @@ -6347,21 +6344,21 @@ __metadata: linkType: hard "@types/send@npm:*": - version: 1.2.0 - resolution: "@types/send@npm:1.2.0" + version: 1.2.1 + resolution: "@types/send@npm:1.2.1" dependencies: "@types/node": "*" - checksum: 73d8dbdeeee748e28c4bc8d5b09ba31a5f1438c278f2c4326d9828aadacaff61430f8b2c3796140ae5abe64eac30df484391b35a1b150cdaa5105c0b050a00a8 + checksum: 3b8388edeec77ae62f7bbc384c98ca06140614e4ef34fc04b35824f19937f472f8ff3785e83570e0d40e6d7c934c015d4831c82a74a1ade0d9676720835702c5 languageName: node linkType: hard "@types/send@npm:<1": - version: 0.17.5 - resolution: "@types/send@npm:0.17.5" + version: 0.17.6 + resolution: "@types/send@npm:0.17.6" dependencies: "@types/mime": ^1 "@types/node": "*" - checksum: bff5add75eb178c3b80bebc422db483c76eeb2cb5016508c952e4fc67d968794f9c709b978d086bf60e4d6fbfe8c0b77e99a7603a615c671c1f97f808458d4a8 + checksum: 5bd287f1357380963eb4b12daef5c8982f52a3269308ff3414304074d4ad7f05fe466f2cb476f54798096877ad3c5343692978776bd674b25261ecbeab87640f languageName: node linkType: hard @@ -6374,14 +6371,14 @@ __metadata: languageName: node linkType: hard -"@types/serve-static@npm:*, @types/serve-static@npm:^1.13.10": - version: 1.15.9 - resolution: "@types/serve-static@npm:1.15.9" +"@types/serve-static@npm:^1, @types/serve-static@npm:^1.13.10": + version: 1.15.10 + resolution: "@types/serve-static@npm:1.15.10" dependencies: "@types/http-errors": "*" "@types/node": "*" "@types/send": <1 - checksum: c7d82c3f24a8db2abca751c45d514e516d02c50aeff36f78eb083134015447122431e3613feac377ec7218eaab683bb9d5d7daa157b76a4a8f11d6d39c042001 + checksum: f216eef2aaf2c8eff09f431c420c5c2989eaf0dfc15d106db9fb64c14577a4059af24fb0ae2eba7984d6360950c8cbc1fb52f65608106477729d251481bc96fe languageName: node linkType: hard @@ -6401,7 +6398,7 @@ __metadata: languageName: node linkType: hard -"@types/statuses@npm:^2.0.4": +"@types/statuses@npm:^2.0.6": version: 2.0.6 resolution: "@types/statuses@npm:2.0.6" checksum: dadfbb4f32a16d3106a8e2a957dab17b1ae99fc4788e1fd96aeaf82355e3b2b069d5ea0f5fb887efa830def033828955a5bbdfd35454ba2088822537aed9e5db @@ -6481,29 +6478,29 @@ __metadata: linkType: hard "@types/yargs@npm:^15.0.4": - version: 15.0.19 - resolution: "@types/yargs@npm:15.0.19" + version: 15.0.20 + resolution: "@types/yargs@npm:15.0.20" dependencies: "@types/yargs-parser": "*" - checksum: 6a509db36304825674f4f00300323dce2b4d850e75819c3db87e9e9f213ac2c4c6ed3247a3e4eed6e8e45b3f191b133a356d3391dd694d9ea27a0507d914ef4c + checksum: 7e33bed59f7d44f32f6c0f6da07e8aa79605d725fcdd223febe45ccfa5254da3bc0f70242553021fd9491b637ae99ddee84e5dd05d1771a71986619a73cbf897 languageName: node linkType: hard "@types/yargs@npm:^16.0.0": - version: 16.0.9 - resolution: "@types/yargs@npm:16.0.9" + version: 16.0.11 + resolution: "@types/yargs@npm:16.0.11" dependencies: "@types/yargs-parser": "*" - checksum: 00d9276ed4e0f17a78c1ed57f644a8c14061959bd5bfab113d57f082ea4b663ba97f71b89371304a34a2dba5061e9ae4523e357e577ba61834d661f82c223bf8 + checksum: d172a63bb046d6ca5dd944a6490789103db288d5b4c78cd0a79f4c1c63718df58d3fd9d1987e57721a58dfc8270a2018b1e93565f930347d41ec15416f3075d9 languageName: node linkType: hard "@types/yargs@npm:^17.0.8": - version: 17.0.33 - resolution: "@types/yargs@npm:17.0.33" + version: 17.0.35 + resolution: "@types/yargs@npm:17.0.35" dependencies: "@types/yargs-parser": "*" - checksum: ee013f257472ab643cb0584cf3e1ff9b0c44bca1c9ba662395300a7f1a6c55fa9d41bd40ddff42d99f5d95febb3907c9ff600fbcb92dadbec22c6a76de7e1236 + checksum: ebf1f5373388cfcbf9cfb5e56ce7a77c0ba2450420f26f3701010ca92df48cce7e14e4245ed1f17178a38ff8702467a6f4047742775b8e2fd06dec8f4f3501ce languageName: node linkType: hard @@ -7110,10 +7107,10 @@ __metadata: languageName: node linkType: hard -"abbrev@npm:^3.0.0": - version: 3.0.1 - resolution: "abbrev@npm:3.0.1" - checksum: e70b209f5f408dd3a3bbd0eec4b10a2ffd64704a4a3821d0969d84928cc490a8eb60f85b78a95622c1841113edac10161c62e52f5e7d0027aa26786a8136e02e +"abbrev@npm:^4.0.0": + version: 4.0.0 + resolution: "abbrev@npm:4.0.0" + checksum: d0344b63d28e763f259b4898c41bdc92c08e9d06d0da5617d0bbe4d78244e46daea88c510a2f9472af59b031d9060ec1a999653144e793fd029a59dae2f56dc8 languageName: node linkType: hard @@ -7363,13 +7360,6 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.1.0": - version: 6.2.3 - resolution: "ansi-styles@npm:6.2.3" - checksum: f1b0829cf048cce870a305819f65ce2adcebc097b6d6479e12e955fd6225df9b9eb8b497083b764df796d94383ff20016cc4dbbae5b40f36138fb65a9d33c2e2 - languageName: node - linkType: hard - "any-promise@npm:^1.0.0": version: 1.3.0 resolution: "any-promise@npm:1.3.0" @@ -7648,12 +7638,12 @@ __metadata: linkType: hard "autoprefixer@npm:^10.4.13": - version: 10.4.21 - resolution: "autoprefixer@npm:10.4.21" + version: 10.4.22 + resolution: "autoprefixer@npm:10.4.22" dependencies: - browserslist: ^4.24.4 - caniuse-lite: ^1.0.30001702 - fraction.js: ^4.3.7 + browserslist: ^4.27.0 + caniuse-lite: ^1.0.30001754 + fraction.js: ^5.3.4 normalize-range: ^0.1.2 picocolors: ^1.1.1 postcss-value-parser: ^4.2.0 @@ -7661,7 +7651,7 @@ __metadata: postcss: ^8.1.0 bin: autoprefixer: bin/autoprefixer - checksum: 11770ce635a0520e457eaf2ff89056cd57094796a9f5d6d9375513388a5a016cd947333dcfd213b822fdd8a0b43ce68ae4958e79c6f077c41d87444c8cca0235 + checksum: 7aa0c668bc1505a81d31e45c74b9f6176c7c4af80e2f9a471c7bcce223742f62de1889cc99f24f7b3ba2117e899e531b12e65f4bc52a29443c9eeb7929878362 languageName: node linkType: hard @@ -7682,13 +7672,13 @@ __metadata: linkType: hard "axios@npm:^1.11.0, axios@npm:^1.7.9": - version: 1.12.2 - resolution: "axios@npm:1.12.2" + version: 1.13.2 + resolution: "axios@npm:1.13.2" dependencies: follow-redirects: ^1.15.6 form-data: ^4.0.4 proxy-from-env: ^1.1.0 - checksum: f0331594fe053a4bbff04104edb073973a3aabfad2e56b0aa18de82428aa63f6f0839ca3d837258ec739cb4528014121793b1649a21e5115ffb2bf8237eadca3 + checksum: 057d0204d5930e2969f0bccb9f0752745b1524a36994667833195e7e1a82f245d660752ba8517b2dbea17e9e4ed0479f10b80c5fe45edd0b5a0df645c0060386 languageName: node linkType: hard @@ -7914,6 +7904,7 @@ __metadata: multer: ^1.4.5-lts.1 nodemailer: ^6.9.1 nodemon: ^2.0.16 + pdf-parse-new: ^1.4.1 prisma: ^6.2.1 shared: 1.0.0 supertest: ^6.2.4 @@ -7944,12 +7935,12 @@ __metadata: languageName: node linkType: hard -"baseline-browser-mapping@npm:^2.8.9": - version: 2.8.17 - resolution: "baseline-browser-mapping@npm:2.8.17" +"baseline-browser-mapping@npm:^2.8.25": + version: 2.8.31 + resolution: "baseline-browser-mapping@npm:2.8.31" bin: baseline-browser-mapping: dist/cli.js - checksum: 2ff31d36b475b628b551e0b29b2fb1ac36f903b99392da4da208ef718beefdc659b24b612a2922664fcffa1fc9bf733bb52d2e29756dcf09c54d764c64f0b964 + checksum: be21b495093f577c6602882b6c3ecf04a1fe4fcf1d1dd497b51571adcc5815999247f3587a85d194b26f4afad3f726e0fd9d2569087ff09146b044ef8044c9de languageName: node linkType: hard @@ -8083,9 +8074,9 @@ __metadata: linkType: hard "bowser@npm:^2.11.0": - version: 2.12.1 - resolution: "bowser@npm:2.12.1" - checksum: 994a3da9e9b628892e0fbc4fd5afeec672003a9a72300ec8ac832f6707ba6ce68d137d50316f08e6197f9e0cca5c486aa4b9ce9db50013061225cab4e432f8a0 + version: 2.13.0 + resolution: "bowser@npm:2.13.0" + checksum: 1eabb028305161d0d6e7826e4938ac69c99e5a36fe07779f5622d350b6ab04160624cd6dedbb862f1d1afdf3096f357e6fec62adc23ac80892feeebd7ecacf68 languageName: node linkType: hard @@ -8140,18 +8131,18 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.18.1, browserslist@npm:^4.21.4, browserslist@npm:^4.24.0, browserslist@npm:^4.24.4, browserslist@npm:^4.26.3": - version: 4.26.3 - resolution: "browserslist@npm:4.26.3" +"browserslist@npm:^4.0.0, browserslist@npm:^4.18.1, browserslist@npm:^4.21.4, browserslist@npm:^4.24.0, browserslist@npm:^4.26.3, browserslist@npm:^4.27.0, browserslist@npm:^4.28.0": + version: 4.28.0 + resolution: "browserslist@npm:4.28.0" dependencies: - baseline-browser-mapping: ^2.8.9 - caniuse-lite: ^1.0.30001746 - electron-to-chromium: ^1.5.227 - node-releases: ^2.0.21 - update-browserslist-db: ^1.1.3 + baseline-browser-mapping: ^2.8.25 + caniuse-lite: ^1.0.30001754 + electron-to-chromium: ^1.5.249 + node-releases: ^2.0.27 + update-browserslist-db: ^1.1.4 bin: browserslist: cli.js - checksum: aa5bbcda9db1eeb9952b4c2f11f9a5a2247da7bcce7fa14d3cc215e67246a93394eda2f86378a41c3f73e6e1a1561bf0e7eade93c5392cb6d37bc66f70d0c53f + checksum: c19fe2c6f123851d899d5b207f76de93064e247931e32ca0adcaceb3b48aec65c7c3310c0dc969de96922d488af7de0a0b77b41505e7dfa0a8d3736500748e11 languageName: node linkType: hard @@ -8215,7 +8206,7 @@ __metadata: languageName: node linkType: hard -"bytes@npm:3.1.2, bytes@npm:^3.1.2": +"bytes@npm:3.1.2, bytes@npm:^3.1.2, bytes@npm:~3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" checksum: e4bcd3948d289c5127591fbedf10c0b639ccbf00243504e4e127374a15c3bc8eed0d28d4aaab08ff6f1cf2abc0cce6ba3085ed32f4f90e82a5683ce0014e1b6e @@ -8254,23 +8245,22 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^19.0.1": - version: 19.0.1 - resolution: "cacache@npm:19.0.1" +"cacache@npm:^20.0.1": + version: 20.0.3 + resolution: "cacache@npm:20.0.3" dependencies: - "@npmcli/fs": ^4.0.0 + "@npmcli/fs": ^5.0.0 fs-minipass: ^3.0.0 - glob: ^10.2.2 - lru-cache: ^10.0.1 + glob: ^13.0.0 + lru-cache: ^11.1.0 minipass: ^7.0.3 minipass-collect: ^2.0.1 minipass-flush: ^1.0.5 minipass-pipeline: ^1.2.4 p-map: ^7.0.2 - ssri: ^12.0.0 - tar: ^7.4.3 - unique-filename: ^4.0.0 - checksum: e95684717de6881b4cdaa949fa7574e3171946421cd8291769dd3d2417dbf7abf4aa557d1f968cca83dcbc95bed2a281072b09abfc977c942413146ef7ed4525 + ssri: ^13.0.0 + unique-filename: ^5.0.0 + checksum: 595e6b91d72972d596e1e9ccab8ddbf08b773f27240220b1b5b1b7b3f52173cfbcf095212e5d7acd86c3bd453c28e69b116469889c511615ef3589523d542639 languageName: node linkType: hard @@ -8356,17 +8346,17 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001702, caniuse-lite@npm:^1.0.30001746": - version: 1.0.30001751 - resolution: "caniuse-lite@npm:1.0.30001751" - checksum: d11e25c44e40c21e7b7492a25c9fd60f4c04e94aa265573f7c487666f5e1b5ca3ed09d09560336f959237063616255cb294d415511bb6cf0486eb2cb6a3a4318 +"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001754": + version: 1.0.30001756 + resolution: "caniuse-lite@npm:1.0.30001756" + checksum: abcbb01eceb921f3462721792ede7ab9dc95eb676e2f6873837ebf5c163f5fe74a629de20b56b8bd10147af2ba255a608d422e72597149e19c91b9b760593bb6 languageName: node linkType: hard "canvas-confetti@npm:^1.9.3": - version: 1.9.3 - resolution: "canvas-confetti@npm:1.9.3" - checksum: db044a9c9ca0e58eafd115f7dfc2f9ecc377be34d8a5dd75901dae0dafd4fb0b75fbbf8edd48bbefad2468653c5838348f0e768b79727a924259a9ada343ea30 + version: 1.9.4 + resolution: "canvas-confetti@npm:1.9.4" + checksum: 61f069b220ee666ec56e5403a7118f1c22c4d55597da3ebfdc8227acdb432ecb9e1bae4b4c86fd15b001422240939d591e44b501dc68ddd545e14f373ab7488e languageName: node linkType: hard @@ -8933,11 +8923,9 @@ __metadata: linkType: hard "content-disposition@npm:^1.0.0": - version: 1.0.0 - resolution: "content-disposition@npm:1.0.0" - dependencies: - safe-buffer: 5.2.1 - checksum: b27e2579fefe0ecf78238bb652fbc750671efce8344f0c6f05235b12433e6a965adb40906df1ac1fdde23e8f9f0e58385e44640e633165420f3f47d830ae0398 + version: 1.0.1 + resolution: "content-disposition@npm:1.0.1" + checksum: f1ee5363968e7e4c491fcd9796d3c489ab29c4ea0bfa5dcc3379a9833d6044838367cf8a11c90b179cb2a8d471279ab259119c52e0d3e4ed30934ccd56b6d694 languageName: node linkType: hard @@ -9015,25 +9003,25 @@ __metadata: linkType: hard "core-js-compat@npm:^3.43.0": - version: 3.46.0 - resolution: "core-js-compat@npm:3.46.0" + version: 3.47.0 + resolution: "core-js-compat@npm:3.47.0" dependencies: - browserslist: ^4.26.3 - checksum: 16d381c51e34d38ecc65d429d5a5c1dbd198f70b5a0a6256a3a41dcb8523e07f0a8682f6349298a55ff6e9d039e131d67b07fe863047a28672ae5f10373c57cf + browserslist: ^4.28.0 + checksum: 425c8cb4c3277a11f3d7d4752c53e5903892635126ed1cdc326a1cd7d961606c5d2c951493f1c783e624f9cdc1ec791c6db68dc19988d68f112d7d82a4c39c9a languageName: node linkType: hard "core-js-pure@npm:^3.23.3": - version: 3.46.0 - resolution: "core-js-pure@npm:3.46.0" - checksum: 5b7903d1a2fb08856b6dd2ebfbca608e025b0c29ebb90f1b0fc9627c2ac1565f12c427db44a1150f555685e669631ea83c60f90d38b5152bd568e9849e2793cf + version: 3.47.0 + resolution: "core-js-pure@npm:3.47.0" + checksum: e3d604ef8f8aaafcd9b8fbc1ac313ea2be21439b84ca8ebebdd7b2a66fddb93e3c52003e5a288514d8735cb9ad1a019b5d046fbcc55cac97c17ba4691fa26f22 languageName: node linkType: hard "core-js@npm:^3.19.2": - version: 3.46.0 - resolution: "core-js@npm:3.46.0" - checksum: 03a3f1b094e8d8baa78081cdcbe2d49b608d612d2f940668c9063e43e52ff19d8e1c8137bdf79734a39445482f981e8d52653f8879539aaff49cf5d85e7183cc + version: 3.47.0 + resolution: "core-js@npm:3.47.0" + checksum: 33ed738fbf1d8596400915ed8ff02538cc89e805d7298e52dbac34b9aecd62400cf84905ce6d5fabd5cc96cb61395907d67d8b89067263f3d7fff8e79a230109 languageName: node linkType: hard @@ -9087,7 +9075,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": +"cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" dependencies: @@ -9392,9 +9380,9 @@ __metadata: linkType: hard "csstype@npm:^3.0.2, csstype@npm:^3.1.3": - version: 3.1.3 - resolution: "csstype@npm:3.1.3" - checksum: 8db785cc92d259102725b3c694ec0c823f5619a84741b5c7991b8ad135dfaa66093038a1cc63e03361a6cd28d122be48f2106ae72334e067dd619a51f49eddf7 + version: 3.2.3 + resolution: "csstype@npm:3.2.3" + checksum: cb882521b3398958a1ce6ca98c011aec0bde1c77ecaf8a1dd4db3b112a189939beae3b1308243b2fe50fc27eb3edeb0f73a5a4d91d928765dc6d5ecc7bda92ee languageName: node linkType: hard @@ -9559,9 +9547,9 @@ __metadata: linkType: hard "dayjs@npm:^1.11.10": - version: 1.11.18 - resolution: "dayjs@npm:1.11.18" - checksum: cc90054bad30ab011417a7a474b2ffa70e7a28ca6f834d7e86fe53a408a40a14c174f26155072628670e9eda4c48c4ed0d847d2edf83d47c0bfb78be15bbf2dd + version: 1.11.19 + resolution: "dayjs@npm:1.11.19" + checksum: dfafcca2c67cc6e542fd880d77f1d91667efd323edc28f0487b470b184a11cc97696163ed5be1142ea2a031045b27a0d0555e72f60a63275e0e0401ac24bea5d languageName: node linkType: hard @@ -9728,7 +9716,7 @@ __metadata: languageName: node linkType: hard -"depd@npm:2.0.0, depd@npm:^2.0.0": +"depd@npm:2.0.0, depd@npm:^2.0.0, depd@npm:~2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" checksum: abbe19c768c97ee2eed6282d8ce3031126662252c58d711f646921c9623f9052e3e1906443066beec1095832f534e57c523b7333f8e7e0d93051ab6baef5ab3a @@ -10052,13 +10040,6 @@ __metadata: languageName: node linkType: hard -"eastasianwidth@npm:^0.2.0": - version: 0.2.0 - resolution: "eastasianwidth@npm:0.2.0" - checksum: 7d00d7cd8e49b9afa762a813faac332dee781932d6f2c848dc348939c4253f1d4564341b7af1d041853bc3f32c2ef141b58e0a4d9862c17a7f08f68df1e0f1ed - languageName: node - linkType: hard - "ecdsa-sig-formatter@npm:1.0.11, ecdsa-sig-formatter@npm:^1.0.11": version: 1.0.11 resolution: "ecdsa-sig-formatter@npm:1.0.11" @@ -10075,13 +10056,13 @@ __metadata: languageName: node linkType: hard -"effect@npm:3.16.12": - version: 3.16.12 - resolution: "effect@npm:3.16.12" +"effect@npm:3.18.4": + version: 3.18.4 + resolution: "effect@npm:3.18.4" dependencies: "@standard-schema/spec": ^1.0.0 fast-check: ^3.23.1 - checksum: e4d2e134e8e92480c9bf741ee259932fd72f06dd8582dc31b4bdd36c0b5e6c5e988e0f68292d3dfcf822b3fd85a3393ad283e0b4835c699a63d3f258c685eead + checksum: d3623a2caff3f9ce62a89c0e1847d336572a60b76bd7437f8563b4d750ab70edea7862b460ff84ff3d9791646052e246e465736767324d5e2c3855a6a5f88f74 languageName: node linkType: hard @@ -10096,10 +10077,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.5.227": - version: 1.5.237 - resolution: "electron-to-chromium@npm:1.5.237" - checksum: 5905e2808dc6243ced0a83537afbafedec20c063feb6403a678b612a7855d79bc6ecb7d094bdab71f54173cf2ae5d1d8070b0c31572025001c94de62af84f5f8 +"electron-to-chromium@npm:^1.5.249": + version: 1.5.259 + resolution: "electron-to-chromium@npm:1.5.259" + checksum: 8711e642257f83d3f17b15917389e95a4da047f680976c48fb00eb6a6868e55f2d300eb6359be745a93ef0fe7720e93a72302f199284039bc453bcb1684772e6 languageName: node linkType: hard @@ -10635,35 +10616,35 @@ __metadata: linkType: hard "esbuild@npm:^0.25.0": - version: 0.25.11 - resolution: "esbuild@npm:0.25.11" - dependencies: - "@esbuild/aix-ppc64": 0.25.11 - "@esbuild/android-arm": 0.25.11 - "@esbuild/android-arm64": 0.25.11 - "@esbuild/android-x64": 0.25.11 - "@esbuild/darwin-arm64": 0.25.11 - "@esbuild/darwin-x64": 0.25.11 - "@esbuild/freebsd-arm64": 0.25.11 - "@esbuild/freebsd-x64": 0.25.11 - "@esbuild/linux-arm": 0.25.11 - "@esbuild/linux-arm64": 0.25.11 - "@esbuild/linux-ia32": 0.25.11 - "@esbuild/linux-loong64": 0.25.11 - "@esbuild/linux-mips64el": 0.25.11 - "@esbuild/linux-ppc64": 0.25.11 - "@esbuild/linux-riscv64": 0.25.11 - "@esbuild/linux-s390x": 0.25.11 - "@esbuild/linux-x64": 0.25.11 - "@esbuild/netbsd-arm64": 0.25.11 - "@esbuild/netbsd-x64": 0.25.11 - "@esbuild/openbsd-arm64": 0.25.11 - "@esbuild/openbsd-x64": 0.25.11 - "@esbuild/openharmony-arm64": 0.25.11 - "@esbuild/sunos-x64": 0.25.11 - "@esbuild/win32-arm64": 0.25.11 - "@esbuild/win32-ia32": 0.25.11 - "@esbuild/win32-x64": 0.25.11 + version: 0.25.12 + resolution: "esbuild@npm:0.25.12" + dependencies: + "@esbuild/aix-ppc64": 0.25.12 + "@esbuild/android-arm": 0.25.12 + "@esbuild/android-arm64": 0.25.12 + "@esbuild/android-x64": 0.25.12 + "@esbuild/darwin-arm64": 0.25.12 + "@esbuild/darwin-x64": 0.25.12 + "@esbuild/freebsd-arm64": 0.25.12 + "@esbuild/freebsd-x64": 0.25.12 + "@esbuild/linux-arm": 0.25.12 + "@esbuild/linux-arm64": 0.25.12 + "@esbuild/linux-ia32": 0.25.12 + "@esbuild/linux-loong64": 0.25.12 + "@esbuild/linux-mips64el": 0.25.12 + "@esbuild/linux-ppc64": 0.25.12 + "@esbuild/linux-riscv64": 0.25.12 + "@esbuild/linux-s390x": 0.25.12 + "@esbuild/linux-x64": 0.25.12 + "@esbuild/netbsd-arm64": 0.25.12 + "@esbuild/netbsd-x64": 0.25.12 + "@esbuild/openbsd-arm64": 0.25.12 + "@esbuild/openbsd-x64": 0.25.12 + "@esbuild/openharmony-arm64": 0.25.12 + "@esbuild/sunos-x64": 0.25.12 + "@esbuild/win32-arm64": 0.25.12 + "@esbuild/win32-ia32": 0.25.12 + "@esbuild/win32-x64": 0.25.12 dependenciesMeta: "@esbuild/aix-ppc64": optional: true @@ -10719,7 +10700,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 397a46d021a0e36246f986c0e4b0b199b48b9106754d7b8e809efa7ca43122726ebeb62a6f90201e97b4b98ba3fc60f5cf33fd167bdab4a7d16a7a016a674522 + checksum: 3d1dc181338e2c44f4374508e9d0da3e7ae90f65d7f3f5d8076ff401a1726c5c9ecc86cfc825249349f1652e12d5ae13f02bcaa4d9487c88c7a11167f52ba353 languageName: node linkType: hard @@ -11493,9 +11474,9 @@ __metadata: linkType: hard "exsolve@npm:^1.0.7": - version: 1.0.7 - resolution: "exsolve@npm:1.0.7" - checksum: 3adce048e4b1b08580aaabf38c7f92f78e1a662a1776fc02d7e9500d5ce4a30cd3f8e62206768821aa2c3bc2411a699146ebc5710ccc3d46e91199dbfff89f54 + version: 1.0.8 + resolution: "exsolve@npm:1.0.8" + checksum: e3a19cde5ffe787b2e970cb8dfadd69cc69a3b5e8b976312d9d8c421bb63b6ac2025cb62356c835bfdd810ea657705ed1db69552506d5f6004a0825480feb256 languageName: node linkType: hard @@ -11530,9 +11511,9 @@ __metadata: linkType: hard "fast-equals@npm:^5.0.1": - version: 5.3.2 - resolution: "fast-equals@npm:5.3.2" - checksum: feffb528d089800a3ae0ad405736b7a182ed96229bedbca79b921cc4fcbc3e7a297e37f24cc4075698abb710b7926ac09dd7f5b260a6414c8734872fb43d2cc6 + version: 5.3.3 + resolution: "fast-equals@npm:5.3.3" + checksum: 52f9e48d12185abe112d54653b8a410845560db2f05fd2140e4f993f47283c060b9ec63eab72c254091bb601103b081912b6a6385e882c09fd686806bd2bf476 languageName: node linkType: hard @@ -11832,16 +11813,6 @@ __metadata: languageName: node linkType: hard -"foreground-child@npm:^3.1.0, foreground-child@npm:^3.3.1": - version: 3.3.1 - resolution: "foreground-child@npm:3.3.1" - dependencies: - cross-spawn: ^7.0.6 - signal-exit: ^4.0.1 - checksum: b2c1a6fc0bf0233d645d9fefdfa999abf37db1b33e5dab172b3cbfb0662b88bfbd2c9e7ab853533d199050ec6b65c03fcf078fc212d26e4990220e98c6930eef - languageName: node - linkType: hard - "fork-ts-checker-webpack-plugin@npm:^6.5.0": version: 6.5.3 resolution: "fork-ts-checker-webpack-plugin@npm:6.5.3" @@ -11887,15 +11858,15 @@ __metadata: linkType: hard "form-data@npm:^4.0.0, form-data@npm:^4.0.4": - version: 4.0.4 - resolution: "form-data@npm:4.0.4" + version: 4.0.5 + resolution: "form-data@npm:4.0.5" dependencies: asynckit: ^0.4.0 combined-stream: ^1.0.8 es-set-tostringtag: ^2.1.0 hasown: ^2.0.2 mime-types: ^2.1.12 - checksum: 9b7788836df9fa5a6999e0c02515b001946b2a868cfe53f026c69e2c537a2ff9fbfb8e9d2b678744628f3dc7a2d6e14e4e45dfaf68aa6239727f0bdb8ce0abf2 + checksum: af8328413c16d0cded5fccc975a44d227c5120fd46a9e81de8acf619d43ed838414cc6d7792195b30b248f76a65246949a129a4dadd148721948f90cd6d4fb69 languageName: node linkType: hard @@ -11918,10 +11889,10 @@ __metadata: languageName: node linkType: hard -"fraction.js@npm:^4.3.7": - version: 4.3.7 - resolution: "fraction.js@npm:4.3.7" - checksum: e1553ae3f08e3ba0e8c06e43a3ab20b319966dfb7ddb96fd9b5d0ee11a66571af7f993229c88ebbb0d4a816eb813a24ed48207b140d442a8f76f33763b8d1f3f +"fraction.js@npm:^5.3.4": + version: 5.3.4 + resolution: "fraction.js@npm:5.3.4" + checksum: 6ac88ecfdb5fabe3566ae30f79828d448288efbb852cd43ad83afc961fb6923e1d77bc65fbcba8ccda10894114edd419581a050c73d61e368fdd4c3ff416a65a languageName: node linkType: hard @@ -12302,35 +12273,14 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2, glob@npm:^10.3.10": - version: 10.4.5 - resolution: "glob@npm:10.4.5" - dependencies: - foreground-child: ^3.1.0 - jackspeak: ^3.1.2 - minimatch: ^9.0.4 - minipass: ^7.1.2 - package-json-from-dist: ^1.0.0 - path-scurry: ^1.11.1 - bin: - glob: dist/esm/bin.mjs - checksum: 0bc725de5e4862f9f387fd0f2b274baf16850dcd2714502ccf471ee401803997983e2c05590cb65f9675a3c6f2a58e7a53f9e365704108c6ad3cbf1d60934c4a - languageName: node - linkType: hard - -"glob@npm:^11.0.0": - version: 11.0.3 - resolution: "glob@npm:11.0.3" +"glob@npm:^13.0.0": + version: 13.0.0 + resolution: "glob@npm:13.0.0" dependencies: - foreground-child: ^3.3.1 - jackspeak: ^4.1.1 - minimatch: ^10.0.3 + minimatch: ^10.1.1 minipass: ^7.1.2 - package-json-from-dist: ^1.0.0 path-scurry: ^2.0.0 - bin: - glob: dist/esm/bin.mjs - checksum: 65ddc1e3c969e87999880580048763cc8b5bdd375930dd43b8100a5ba481d2e2563e4553de42875790800c602522a98aa8d3ed1c5bd4d27621609e6471eb371d + checksum: 963730222b0acc85a0d2616c08ba3a5d5b5f33fbf69182791967b8a02245db505577a6fc19836d5d58e1cbbfb414ad4f62f605a0372ab05cd9e6998efe944369 languageName: node linkType: hard @@ -12502,10 +12452,10 @@ __metadata: languageName: node linkType: hard -"graphql@npm:^16.8.1": - version: 16.11.0 - resolution: "graphql@npm:16.11.0" - checksum: 65bc206edbe980f2759a8e4cf324873f75a66ab48263961472716e50127ae446739be20f926bb7f036a8d199bd4de072f684fd147c285bd6ba965d98cebb6200 +"graphql@npm:^16.12.0": + version: 16.12.0 + resolution: "graphql@npm:16.12.0" + checksum: c0d2435425270c575091861c9fd82d7cebc1fb1bd5461e05c36521a988f69c5074461e27b89ab70851fabc72ec9d988235f288ba7bbeff67d08a973e8b9d6d3d languageName: node linkType: hard @@ -12765,8 +12715,8 @@ __metadata: linkType: hard "html-webpack-plugin@npm:^5.5.0": - version: 5.6.4 - resolution: "html-webpack-plugin@npm:5.6.4" + version: 5.6.5 + resolution: "html-webpack-plugin@npm:5.6.5" dependencies: "@types/html-minifier-terser": ^6.0.0 html-minifier-terser: ^6.0.2 @@ -12781,7 +12731,7 @@ __metadata: optional: true webpack: optional: true - checksum: b486d99ce820d563dda215f8b6bbeb78127a738b88b419c95d577165e10dd29125e151010b5745ce933e8055f2445c2f9ad1c93024ad3b752db9ac613e84cfac + checksum: 74fa9b1a25cb1e5434d22bbc7cce8d7a182f2db9061accf8b535cdb676e251bd0d9f3d7efd6aaab0988157acc8c8349a435c6fd0d4f2c21f5d24c2b3c5c9f611 languageName: node linkType: hard @@ -12811,7 +12761,7 @@ __metadata: languageName: node linkType: hard -"http-errors@npm:2.0.0, http-errors@npm:^2.0.0": +"http-errors@npm:2.0.0": version: 2.0.0 resolution: "http-errors@npm:2.0.0" dependencies: @@ -12824,6 +12774,19 @@ __metadata: languageName: node linkType: hard +"http-errors@npm:^2.0.0, http-errors@npm:~2.0.1": + version: 2.0.1 + resolution: "http-errors@npm:2.0.1" + dependencies: + depd: ~2.0.0 + inherits: ~2.0.4 + setprototypeof: ~1.2.0 + statuses: ~2.0.2 + toidentifier: ~1.0.1 + checksum: 155d1a100a06e4964597013109590b97540a177b69c3600bbc93efc746465a99a2b718f43cdf76b3791af994bbe3a5711002046bf668cdc007ea44cea6df7ccd + languageName: node + linkType: hard + "http-errors@npm:~1.6.2": version: 1.6.3 resolution: "http-errors@npm:1.6.3" @@ -12929,21 +12892,21 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.7.0": - version: 0.7.0 - resolution: "iconv-lite@npm:0.7.0" +"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" dependencies: safer-buffer: ">= 2.1.2 < 3.0.0" - checksum: f362a8befb95e37f29be1d1290c17e0c9d0d4ad4fa62fcfd813cc9c937ab89401abed9a011f83e10651a267abb2aa231ec7da91d843570bec873bd98489b5bf8 + checksum: 3f60d47a5c8fc3313317edfd29a00a692cc87a19cac0159e2ce711d0ebc9019064108323b5e493625e25594f11c6236647d8e256fbe7a58f4a3b33b89e6d30bf languageName: node linkType: hard -"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": - version: 0.6.3 - resolution: "iconv-lite@npm:0.6.3" +"iconv-lite@npm:~0.7.0": + version: 0.7.0 + resolution: "iconv-lite@npm:0.7.0" dependencies: safer-buffer: ">= 2.1.2 < 3.0.0" - checksum: 3f60d47a5c8fc3313317edfd29a00a692cc87a19cac0159e2ce711d0ebc9019064108323b5e493625e25594f11c6236647d8e256fbe7a58f4a3b33b89e6d30bf + checksum: f362a8befb95e37f29be1d1290c17e0c9d0d4ad4fa62fcfd813cc9c937ab89401abed9a011f83e10651a267abb2aa231ec7da91d843570bec873bd98489b5bf8 languageName: node linkType: hard @@ -13060,7 +13023,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3": +"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3, inherits@npm:~2.0.4": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 @@ -13081,10 +13044,10 @@ __metadata: languageName: node linkType: hard -"inline-style-parser@npm:0.2.4": - version: 0.2.4 - resolution: "inline-style-parser@npm:0.2.4" - checksum: 5df20a21dd8d67104faaae29774bb50dc9690c75bc5c45dac107559670a5530104ead72c4cf54f390026e617e7014c65b3d68fb0bb573a37c4d1f94e9c36e1ca +"inline-style-parser@npm:0.2.7": + version: 0.2.7 + resolution: "inline-style-parser@npm:0.2.7" + checksum: 7bf0b92aeae1f96f8a4316e38fc7441a885c221cc4b7fc1aa05835004b42a66fa863aaa5f7555a62e47fdf3da02628fbabee89d38b9edddd9e93597a55c18b5d languageName: node linkType: hard @@ -13107,9 +13070,9 @@ __metadata: linkType: hard "ip-address@npm:^10.0.1": - version: 10.0.1 - resolution: "ip-address@npm:10.0.1" - checksum: 525d5391cfd31a91f80f5857e98487aeaa8474e860a6725a0b6461ac8e436c7f8c869774dece391c8f8e7486306a34a4d1c094778c4c583a3f1f2cd905e5ed50 + version: 10.1.0 + resolution: "ip-address@npm:10.1.0" + checksum: 76b1abcdf52a32e2e05ca1f202f3a8ab8547e5651a9233781b330271bd7f1a741067748d71c4cbb9d9906d9f1fa69e7ddc8b4a11130db4534fdab0e908c84e0d languageName: node linkType: hard @@ -13210,7 +13173,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.0, is-core-module@npm:^2.16.1": +"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.1": version: 2.16.1 resolution: "is-core-module@npm:2.16.1" dependencies: @@ -13631,28 +13594,6 @@ __metadata: languageName: node linkType: hard -"jackspeak@npm:^3.1.2": - version: 3.4.3 - resolution: "jackspeak@npm:3.4.3" - dependencies: - "@isaacs/cliui": ^8.0.2 - "@pkgjs/parseargs": ^0.11.0 - dependenciesMeta: - "@pkgjs/parseargs": - optional: true - checksum: be31027fc72e7cc726206b9f560395604b82e0fddb46c4cbf9f97d049bcef607491a5afc0699612eaa4213ca5be8fd3e1e7cd187b3040988b65c9489838a7c00 - languageName: node - linkType: hard - -"jackspeak@npm:^4.1.1": - version: 4.1.1 - resolution: "jackspeak@npm:4.1.1" - dependencies: - "@isaacs/cliui": ^8.0.2 - checksum: daca714c5adebfb80932c0b0334025307b68602765098d73d52ec546bc4defdb083292893384261c052742255d0a77d8fcf96f4c669bcb4a99b498b94a74955e - languageName: node - linkType: hard - "jake@npm:^10.8.5": version: 10.9.4 resolution: "jake@npm:10.9.4" @@ -14364,25 +14305,25 @@ __metadata: linkType: hard "js-yaml@npm:^3.13.1": - version: 3.14.1 - resolution: "js-yaml@npm:3.14.1" + version: 3.14.2 + resolution: "js-yaml@npm:3.14.2" dependencies: argparse: ^1.0.7 esprima: ^4.0.0 bin: js-yaml: bin/js-yaml.js - checksum: bef146085f472d44dee30ec34e5cf36bf89164f5d585435a3d3da89e52622dff0b188a580e4ad091c3341889e14cb88cac6e4deb16dc5b1e9623bb0601fc255c + checksum: 626fc207734a3452d6ba84e1c8c226240e6d431426ed94d0ab043c50926d97c509629c08b1d636f5d27815833b7cfd225865631da9fb33cb957374490bf3e90b languageName: node linkType: hard "js-yaml@npm:^4.1.0": - version: 4.1.0 - resolution: "js-yaml@npm:4.1.0" + version: 4.1.1 + resolution: "js-yaml@npm:4.1.1" dependencies: argparse: ^2.0.1 bin: js-yaml: bin/js-yaml.js - checksum: c7830dfd456c3ef2c6e355cc5a92e6700ceafa1d14bba54497b34a99f0376cecbb3e9ac14d3e5849b426d5a5140709a66237a8c991c675431271c4ce5504151a + checksum: ea2339c6930fe048ec31b007b3c90be2714ab3e7defcc2c27ebf30c74fd940358f29070b4345af0019ef151875bf3bc3f8644bea1bab0372652b5044813ac02d languageName: node linkType: hard @@ -14674,12 +14615,12 @@ __metadata: linkType: hard "launch-editor@npm:^2.6.0": - version: 2.11.1 - resolution: "launch-editor@npm:2.11.1" + version: 2.12.0 + resolution: "launch-editor@npm:2.12.0" dependencies: picocolors: ^1.1.1 shell-quote: ^1.8.3 - checksum: 95a2e0a50ce15425a87fd035bdef2de37e13c2aee9cd62756783efb286a6e36a341cfcbaecb0d578131a5411c6a1c74c422f9c5b6cb6f4c8284d6078967e08b4 + checksum: b1aa1b92ef4e720d1edd7f80affb90b2fa1cc2c41641cf80158940698c18a4b6a67e2a7cb060547712e858f0ec1a7c8c39f605e0eb299f516a6184f4e680ffc8 languageName: node linkType: hard @@ -14738,7 +14679,7 @@ __metadata: languageName: node linkType: hard -"loader-runner@npm:^4.2.0": +"loader-runner@npm:^4.3.1": version: 4.3.1 resolution: "loader-runner@npm:4.3.1" checksum: 14689a39a79b286d3d15f2199384d6132d62ea707abd6c7e50dc8a1f80c20cbfdd5344f7e6b4a7346974696689ab1a96f8ec7d1e8bf206c5264561502658bd3c @@ -14937,14 +14878,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": - version: 10.4.3 - resolution: "lru-cache@npm:10.4.3" - checksum: 6476138d2125387a6d20f100608c2583d415a4f64a0fecf30c9e2dda976614f09cad4baa0842447bd37dd459a7bd27f57d9d8f8ce558805abd487c583f3d774a - languageName: node - linkType: hard - -"lru-cache@npm:^11.0.0": +"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": version: 11.2.2 resolution: "lru-cache@npm:11.2.2" checksum: 052b3d0b81a02dd017e8b6d82422bed273732c89c9c63762f538e0a75b7018247896b365c19d9392cc7de9c6a304cde3ac11eb7376f96a4885d0ab32b5c46d5b @@ -14988,11 +14922,11 @@ __metadata: linkType: hard "magic-string@npm:^0.30.12, magic-string@npm:^0.30.17": - version: 0.30.19 - resolution: "magic-string@npm:0.30.19" + version: 0.30.21 + resolution: "magic-string@npm:0.30.21" dependencies: "@jridgewell/sourcemap-codec": ^1.5.5 - checksum: f360b87febeceddb35238e55963b70ef68381688c1aada6d842833a7be440a08cb0a8776e23b5e4e34785edc6b42b92dc08c829f43ecdb58547122f3fd79fdc7 + checksum: 4ff76a4e8d439431cf49f039658751ed351962d044e5955adc257489569bd676019c906b631f86319217689d04815d7d064ee3ff08ab82ae65b7655a7e82a414 languageName: node linkType: hard @@ -15035,22 +14969,22 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^14.0.3": - version: 14.0.3 - resolution: "make-fetch-happen@npm:14.0.3" +"make-fetch-happen@npm:^15.0.0": + version: 15.0.3 + resolution: "make-fetch-happen@npm:15.0.3" dependencies: - "@npmcli/agent": ^3.0.0 - cacache: ^19.0.1 + "@npmcli/agent": ^4.0.0 + cacache: ^20.0.1 http-cache-semantics: ^4.1.1 minipass: ^7.0.2 - minipass-fetch: ^4.0.0 + minipass-fetch: ^5.0.0 minipass-flush: ^1.0.5 minipass-pipeline: ^1.2.4 negotiator: ^1.0.0 - proc-log: ^5.0.0 + proc-log: ^6.0.0 promise-retry: ^2.0.1 - ssri: ^12.0.0 - checksum: 6fb2fee6da3d98f1953b03d315826b5c5a4ea1f908481afc113782d8027e19f080c85ae998454de4e5f27a681d3ec58d57278f0868d4e0b736f51d396b661691 + ssri: ^13.0.0 + checksum: 4fb9dbb739b33565c85dacdcff7eb9388d8f36f326a59dc13375f01af809c42c48aa5d1f4840ee36623b2461a15476e1e79e4548ca1af30b42e1e324705ac8b3 languageName: node linkType: hard @@ -15159,8 +15093,8 @@ __metadata: linkType: hard "mdast-util-to-hast@npm:^13.0.0": - version: 13.2.0 - resolution: "mdast-util-to-hast@npm:13.2.0" + version: 13.2.1 + resolution: "mdast-util-to-hast@npm:13.2.1" dependencies: "@types/hast": ^3.0.0 "@types/mdast": ^4.0.0 @@ -15171,7 +15105,7 @@ __metadata: unist-util-position: ^5.0.0 unist-util-visit: ^5.0.0 vfile: ^6.0.0 - checksum: 7e5231ff3d4e35e1421908437577fd5098141f64918ff5cc8a0f7a8a76c5407f7a3ee88d75f7a1f7afb763989c9f357475fa0ba8296c00aaff1e940098fe86a6 + checksum: 20537df653be3653c3c6ea4be09ea1f67ca2f5e6afea027fcc3cde531656dc669a5e733d34a95b08b3ee71ab164c7b24352c8212891f723ddcec74d5a046bfd6 languageName: node linkType: hard @@ -15569,11 +15503,11 @@ __metadata: linkType: hard "mime-types@npm:^3.0.0, mime-types@npm:^3.0.1": - version: 3.0.1 - resolution: "mime-types@npm:3.0.1" + version: 3.0.2 + resolution: "mime-types@npm:3.0.2" dependencies: mime-db: ^1.54.0 - checksum: 8d497ad5cb2dd1210ac7d049b5de94af0b24b45a314961e145b44389344604d54752f03bc00bf880c0da60a214be6fb6d423d318104f02c28d95dd8ebeea4fb4 + checksum: 70b74794f408419e4b6a8e3c93ccbed79b6a6053973a3957c5cc04ff4ad8d259f0267da179e3ecae34c3edfb4bfd7528db23a101e32d21ad8e196178c8b7b75a languageName: node linkType: hard @@ -15635,12 +15569,12 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.0.3": - version: 10.0.3 - resolution: "minimatch@npm:10.0.3" +"minimatch@npm:^10.1.1": + version: 10.1.1 + resolution: "minimatch@npm:10.1.1" dependencies: "@isaacs/brace-expansion": ^5.0.0 - checksum: 20bfb708095a321cb43c20b78254e484cb7d23aad992e15ca3234a3331a70fa9cd7a50bc1a7c7b2b9c9890c37ff0685f8380028fcc28ea5e6de75b1d4f9374aa + checksum: 8820c0be92994f57281f0a7a2cc4268dcc4b610f9a1ab666685716b4efe4b5898b43c835a8f22298875b31c7a278a5e3b7e253eee7c886546bb0b61fb94bca6b languageName: node linkType: hard @@ -15687,9 +15621,9 @@ __metadata: languageName: node linkType: hard -"minipass-fetch@npm:^4.0.0": - version: 4.0.1 - resolution: "minipass-fetch@npm:4.0.1" +"minipass-fetch@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass-fetch@npm:5.0.0" dependencies: encoding: ^0.1.13 minipass: ^7.0.3 @@ -15698,7 +15632,7 @@ __metadata: dependenciesMeta: encoding: optional: true - checksum: 3dfca705ce887ca9ff14d73e8d8593996dea1a1ecd8101fdbb9c10549d1f9670bc8fb66ad0192769ead4c2dc01b4f9ca1cf567ded365adff17827a303b948140 + checksum: 416645d1e54c09fdfe64ec1676541ac2f6f2af3abc7ad25f2f22c4518535997c1ecd2c0c586ea8a5c6499ad7d8f97671f50ff38488ada54bf61fde309f731379 languageName: node linkType: hard @@ -15738,7 +15672,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": +"minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": version: 7.1.2 resolution: "minipass@npm:7.1.2" checksum: 2bfd325b95c555f2b4d2814d49325691c7bee937d753814861b0b49d5edcda55cbbf22b6b6a60bb91eddac8668771f03c5ff647dcd9d0f798e9548b9cdc46ee3 @@ -15794,15 +15728,15 @@ __metadata: linkType: hard "msw@npm:^2.7.0": - version: 2.11.5 - resolution: "msw@npm:2.11.5" + version: 2.12.3 + resolution: "msw@npm:2.12.3" dependencies: "@inquirer/confirm": ^5.0.0 - "@mswjs/interceptors": ^0.39.1 + "@mswjs/interceptors": ^0.40.0 "@open-draft/deferred-promise": ^2.2.0 - "@types/statuses": ^2.0.4 + "@types/statuses": ^2.0.6 cookie: ^1.0.2 - graphql: ^16.8.1 + graphql: ^16.12.0 headers-polyfill: ^4.0.2 is-node-process: ^1.2.0 outvariant: ^1.4.3 @@ -15812,7 +15746,7 @@ __metadata: statuses: ^2.0.2 strict-event-emitter: ^0.5.1 tough-cookie: ^6.0.0 - type-fest: ^4.26.1 + type-fest: ^5.2.0 until-async: ^3.0.2 yargs: ^17.7.2 peerDependencies: @@ -15822,7 +15756,7 @@ __metadata: optional: true bin: msw: cli/index.js - checksum: 86eaa1cbd0c9d3f56434f1e8d362802624d7548953cff1f1fcbf3e8a2ec6af827b444762c881de165f13e8665c67787a34831f97e5945d119f060f6fe880bf4c + checksum: b9347dc408d3267d9fb65ebc603c5be4c1c52836834d37f2c60f69f9e630cf1cfbdaf6cdfa7fe1e8c3a9d451028f9b78d9949039fd90ee4cd9628a718204935b languageName: node linkType: hard @@ -15949,11 +15883,11 @@ __metadata: linkType: hard "node-abi@npm:^3.3.0": - version: 3.78.0 - resolution: "node-abi@npm:3.78.0" + version: 3.85.0 + resolution: "node-abi@npm:3.85.0" dependencies: semver: ^7.3.5 - checksum: a630e26729132a9a5a50604e51dc5a2d9c2a3ec2e9698b66a0c777532d08d8e28acb0332eaf5157ee16c7c7874b52624b3281c9077eea15e1ac6c118e7076dac + checksum: 173eb81d61a5844498c89586f1492fb1823e26aa5656e2aff2d427584c7213190314e2d41180a8c3750b5dcf90325796481e8527dad71c5a16f93bc11762d7ac languageName: node linkType: hard @@ -15966,6 +15900,13 @@ __metadata: languageName: node linkType: hard +"node-ensure@npm:^0.0.0": + version: 0.0.0 + resolution: "node-ensure@npm:0.0.0" + checksum: 1124623beb1dec66889794286ec1761ac3bfac42ce93b8652903efa5af14227edce5bafa06c4e0675cdc850360931d7c9413e3b5406150f05b2c9bf5bb3ccce4 + languageName: node + linkType: hard + "node-fetch-native@npm:^1.6.6": version: 1.6.7 resolution: "node-fetch-native@npm:1.6.7" @@ -15995,22 +15936,22 @@ __metadata: linkType: hard "node-gyp@npm:latest": - version: 11.5.0 - resolution: "node-gyp@npm:11.5.0" + version: 12.1.0 + resolution: "node-gyp@npm:12.1.0" dependencies: env-paths: ^2.2.0 exponential-backoff: ^3.1.1 graceful-fs: ^4.2.6 - make-fetch-happen: ^14.0.3 - nopt: ^8.0.0 - proc-log: ^5.0.0 + make-fetch-happen: ^15.0.0 + nopt: ^9.0.0 + proc-log: ^6.0.0 semver: ^7.3.5 - tar: ^7.4.3 + tar: ^7.5.2 tinyglobby: ^0.2.12 - which: ^5.0.0 + which: ^6.0.0 bin: node-gyp: bin/node-gyp.js - checksum: 6cc29b9d454d9a684c8fe299668db618875bb4282e37717ca5b79689cc5ce99cd553c70944bb367979f2eba40ad6a50afaf7b12a6b214172edc7377384efa051 + checksum: 198d91c535fe9940bcdc0db4e578f94cf9872e0d068e88ef2f4656924248bb67245b270b48eded6634c7513841c0cd42f3da3ac9d77c8e16437fcd90703b9ef3 languageName: node linkType: hard @@ -16021,10 +15962,10 @@ __metadata: languageName: node linkType: hard -"node-releases@npm:^2.0.21": - version: 2.0.25 - resolution: "node-releases@npm:2.0.25" - checksum: 9a23149cf3f6778e62440b1f26f91927aff06c3606a29996f3d196c7c0f5e31c17c24c324b5ef1f571cebef6b5a8db9adce9c09381ca271bc6422aac91463f75 +"node-releases@npm:^2.0.27": + version: 2.0.27 + resolution: "node-releases@npm:2.0.27" + checksum: a9a54079d894704c2ec728a690b41fbc779a710f5d47b46fa3e460acff08a3e7dfa7108e5599b2db390aa31dac062c47c5118317201f12784188dc5b415f692d languageName: node linkType: hard @@ -16055,14 +15996,14 @@ __metadata: languageName: node linkType: hard -"nopt@npm:^8.0.0": - version: 8.1.0 - resolution: "nopt@npm:8.1.0" +"nopt@npm:^9.0.0": + version: 9.0.0 + resolution: "nopt@npm:9.0.0" dependencies: - abbrev: ^3.0.0 + abbrev: ^4.0.0 bin: nopt: bin/nopt.js - checksum: 49cfd3eb6f565e292bf61f2ff1373a457238804d5a5a63a8d786c923007498cba89f3648e3b952bc10203e3e7285752abf5b14eaf012edb821e84f24e881a92a + checksum: 7a5d9ab0629eaec1944a95438cc4efa6418ed2834aa8eb21a1bea579a7d8ac3e30120131855376a96ef59ab0e23ad8e0bc94d3349770a95e5cb7119339f7c7fb languageName: node linkType: hard @@ -16405,9 +16346,9 @@ __metadata: linkType: hard "p-map@npm:^7.0.2": - version: 7.0.3 - resolution: "p-map@npm:7.0.3" - checksum: 8c92d533acf82f0d12f7e196edccff773f384098bbb048acdd55a08778ce4fc8889d8f1bde72969487bd96f9c63212698d79744c20bedfce36c5b00b46d369f8 + version: 7.0.4 + resolution: "p-map@npm:7.0.4" + checksum: 4be2097e942f2fd3a4f4b0c6585c721f23851de8ad6484d20c472b3ea4937d5cd9a59914c832b1bceac7bf9d149001938036b82a52de0bc381f61ff2d35d26a5 languageName: node linkType: hard @@ -16447,7 +16388,7 @@ __metadata: languageName: node linkType: hard -"package-json-from-dist@npm:^1.0.0": +"package-json-from-dist@npm:^1.0.1": version: 1.0.1 resolution: "package-json-from-dist@npm:1.0.1" checksum: 58ee9538f2f762988433da00e26acc788036914d57c71c246bf0be1b60cdbd77dd60b6a3e1a30465f0b248aeb80079e0b34cb6050b1dfa18c06953bb1cbc7602 @@ -16576,23 +16517,13 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^1.11.1": - version: 1.11.1 - resolution: "path-scurry@npm:1.11.1" - dependencies: - lru-cache: ^10.2.0 - minipass: ^5.0.0 || ^6.0.2 || ^7.0.0 - checksum: 890d5abcd593a7912dcce7cf7c6bf7a0b5648e3dee6caf0712c126ca0a65c7f3d7b9d769072a4d1baf370f61ce493ab5b038d59988688e0c5f3f646ee3c69023 - languageName: node - linkType: hard - "path-scurry@npm:^2.0.0": - version: 2.0.0 - resolution: "path-scurry@npm:2.0.0" + version: 2.0.1 + resolution: "path-scurry@npm:2.0.1" dependencies: lru-cache: ^11.0.0 minipass: ^7.1.2 - checksum: 9953ce3857f7e0796b187a7066eede63864b7e1dfc14bf0484249801a5ab9afb90d9a58fc533ebb1b552d23767df8aa6a2c6c62caf3f8a65f6ce336a97bbb484 + checksum: a022c6c38fed836079d03f96540eafd4cd989acf287b99613c82300107f366e889513ad8b671a2039a9d251122621f9c6fa649f0bd4d50acf95a6943a6692dbf languageName: node linkType: hard @@ -16673,6 +16604,16 @@ __metadata: languageName: node linkType: hard +"pdf-parse-new@npm:^1.4.1": + version: 1.4.1 + resolution: "pdf-parse-new@npm:1.4.1" + dependencies: + debug: ^4.3.4 + node-ensure: ^0.0.0 + checksum: 77eb3b3896a84c8cf6a75edffc58d0a0f7cbe21dd745e3b6a3291885778f26ab855f03568083037baa7653369283a57cb2a4f49e2764ce34b5c440a59d8fb9a6 + languageName: node + linkType: hard + "pdfjs-dist@npm:4.8.69": version: 4.8.69 resolution: "pdfjs-dist@npm:4.8.69" @@ -17760,11 +17701,11 @@ __metadata: linkType: hard "prisma@npm:^6.2.1": - version: 6.17.1 - resolution: "prisma@npm:6.17.1" + version: 6.19.0 + resolution: "prisma@npm:6.19.0" dependencies: - "@prisma/config": 6.17.1 - "@prisma/engines": 6.17.1 + "@prisma/config": 6.19.0 + "@prisma/engines": 6.19.0 peerDependencies: typescript: ">=5.1.0" peerDependenciesMeta: @@ -17772,14 +17713,14 @@ __metadata: optional: true bin: prisma: build/index.js - checksum: 92fc75da6555e84501ae4823f90aa1bc91b87ebb83afafe60d9e3c322ac64d70dfdd913a49966011f69ec5a62fcb14b88a06e9d4dff30eef67bca726366bb8bf + checksum: f98cbe3c7f272c64b2fbd9a87c40530246d6aa1e7b87ded23e44e3d108f33aabace7ea50ed2eda0b0f7b9aab5caca359c3603768d0812969964d9fadb4a8f6f4 languageName: node linkType: hard -"proc-log@npm:^5.0.0": - version: 5.0.0 - resolution: "proc-log@npm:5.0.0" - checksum: c78b26ecef6d5cce4a7489a1e9923d7b4b1679028c8654aef0463b27f4a90b0946cd598f55799da602895c52feb085ec76381d007ab8dcceebd40b89c2f9dfe0 +"proc-log@npm:^6.0.0": + version: 6.0.0 + resolution: "proc-log@npm:6.0.0" + checksum: 005a2aa24b8cbc98ca24fc49ccbdbbefb1f7932fcc96664e1008c59459ea66e8cc8bd463716d6c67e39327d45e8e080eb73bf32be70461425593b0f0d6acb91f languageName: node linkType: hard @@ -17992,14 +17933,14 @@ __metadata: linkType: hard "raw-body@npm:^3.0.0": - version: 3.0.1 - resolution: "raw-body@npm:3.0.1" + version: 3.0.2 + resolution: "raw-body@npm:3.0.2" dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.7.0 - unpipe: 1.0.0 - checksum: e75e1db74337e01b78cc07f7e65692ed46aef2e0c4fb39cb25bb365a7ab04cbdb8b3541aabebe50b07a5bffec5def5674b587fd9e8fbde84c8838cbab1ad9836 + bytes: ~3.1.2 + http-errors: ~2.0.1 + iconv-lite: ~0.7.0 + unpipe: ~1.0.0 + checksum: bf8ce8e9734f273f24d81f9fed35609dbd25c2869faa5fb5075f7ee225c0913e2240adda03759d7e72f2a757f8012d58bb7a871a80261d5140ad65844caeb5bd languageName: node linkType: hard @@ -18188,11 +18129,11 @@ __metadata: linkType: hard "react-hook-form@npm:^7.34.0": - version: 7.65.0 - resolution: "react-hook-form@npm:7.65.0" + version: 7.66.1 + resolution: "react-hook-form@npm:7.66.1" peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - checksum: ad7b6c6919e0f987587a94fe9bed0bd7d4c6817d7f9c4651f8d9d747261ad99b20d85f2ff2f329b86aadecc1ab48d4b0ef2eb13123e1a5d48022dbca259c4aef + checksum: b0d4e9b3091485f6a92c539e1999a0570c78da9179ddeaa7999f98c547b70c99f3365cc2d3d652a581f8ea26a96f9ad03102c140a11aea4c4e8966cb734ea3af languageName: node linkType: hard @@ -18217,7 +18158,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^19.0.0, react-is@npm:^19.1.1": +"react-is@npm:^19.0.0, react-is@npm:^19.2.0": version: 19.2.0 resolution: "react-is@npm:19.2.0" checksum: 9a23e1c2d0bbc13b383bc59a05f54e6eb95dd87e01aec8aa92a88618364b7b0ee8a5b057ad813cf61e2f7ae7d24503b624706acb609d07c54754e5ad2c522568 @@ -18671,7 +18612,7 @@ __metadata: languageName: node linkType: hard -"regexpu-core@npm:^6.2.0": +"regexpu-core@npm:^6.3.1": version: 6.4.0 resolution: "regexpu-core@npm:6.4.0" dependencies: @@ -18863,15 +18804,15 @@ __metadata: linkType: hard "resolve@npm:^1.1.7, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.10, resolve@npm:^1.22.4, resolve@npm:^1.22.8": - version: 1.22.10 - resolution: "resolve@npm:1.22.10" + version: 1.22.11 + resolution: "resolve@npm:1.22.11" dependencies: - is-core-module: ^2.16.0 + is-core-module: ^2.16.1 path-parse: ^1.0.7 supports-preserve-symlinks-flag: ^1.0.0 bin: resolve: bin/resolve - checksum: ab7a32ff4046fcd7c6fdd525b24a7527847d03c3650c733b909b01b757f92eb23510afa9cc3e9bf3f26a3e073b48c88c706dfd4c1d2fb4a16a96b73b6328ddcf + checksum: 6d5baa2156b95a65ac431e7642e21106584e9f4194da50871cae8bc1bbd2b53bb7cee573c92543d83bb999620b224a087f62379d800ed1ccb189da6df5d78d50 languageName: node linkType: hard @@ -18889,15 +18830,15 @@ __metadata: linkType: hard "resolve@patch:resolve@^1.1.7#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.10#~builtin, resolve@patch:resolve@^1.22.4#~builtin, resolve@patch:resolve@^1.22.8#~builtin": - version: 1.22.10 - resolution: "resolve@patch:resolve@npm%3A1.22.10#~builtin::version=1.22.10&hash=07638b" + version: 1.22.11 + resolution: "resolve@patch:resolve@npm%3A1.22.11#~builtin::version=1.22.11&hash=07638b" dependencies: - is-core-module: ^2.16.0 + is-core-module: ^2.16.1 path-parse: ^1.0.7 supports-preserve-symlinks-flag: ^1.0.0 bin: resolve: bin/resolve - checksum: 8aac1e4e4628bd00bf4b94b23de137dd3fe44097a8d528fd66db74484be929936e20c696e1a3edf4488f37e14180b73df6f600992baea3e089e8674291f16c9d + checksum: 1462da84ac3410d7c2e12e4f5f25c1423d8a174c3b4245c43eafea85e7bbe6af3eb7ec10a4850b5e518e8531608604742b8cbd761e1acd7ad1035108b7c98013 languageName: node linkType: hard @@ -18954,14 +18895,14 @@ __metadata: linkType: hard "rimraf@npm:^6.0.1": - version: 6.0.1 - resolution: "rimraf@npm:6.0.1" + version: 6.1.2 + resolution: "rimraf@npm:6.1.2" dependencies: - glob: ^11.0.0 - package-json-from-dist: ^1.0.0 + glob: ^13.0.0 + package-json-from-dist: ^1.0.1 bin: rimraf: dist/esm/bin.mjs - checksum: 8ba5b84131c1344e9417cb7e8c05d8368bb73cbe5dd4c1d5eb49fc0b558209781658d18c450460e30607d0b7865bb067482839a2f343b186b07ae87715837e66 + checksum: 9b3e6f715b29a0245dcbc81783a7c8bd5a0804d52e0b36099856707df030dd3d31243f6fec2cc24be71bc2cc878139514fc8389d46cb0eb71255219585cd4cfd languageName: node linkType: hard @@ -18994,31 +18935,31 @@ __metadata: linkType: hard "rollup@npm:^4.20.0, rollup@npm:^4.23.0, rollup@npm:^4.43.0": - version: 4.52.4 - resolution: "rollup@npm:4.52.4" - dependencies: - "@rollup/rollup-android-arm-eabi": 4.52.4 - "@rollup/rollup-android-arm64": 4.52.4 - "@rollup/rollup-darwin-arm64": 4.52.4 - "@rollup/rollup-darwin-x64": 4.52.4 - "@rollup/rollup-freebsd-arm64": 4.52.4 - "@rollup/rollup-freebsd-x64": 4.52.4 - "@rollup/rollup-linux-arm-gnueabihf": 4.52.4 - "@rollup/rollup-linux-arm-musleabihf": 4.52.4 - "@rollup/rollup-linux-arm64-gnu": 4.52.4 - "@rollup/rollup-linux-arm64-musl": 4.52.4 - "@rollup/rollup-linux-loong64-gnu": 4.52.4 - "@rollup/rollup-linux-ppc64-gnu": 4.52.4 - "@rollup/rollup-linux-riscv64-gnu": 4.52.4 - "@rollup/rollup-linux-riscv64-musl": 4.52.4 - "@rollup/rollup-linux-s390x-gnu": 4.52.4 - "@rollup/rollup-linux-x64-gnu": 4.52.4 - "@rollup/rollup-linux-x64-musl": 4.52.4 - "@rollup/rollup-openharmony-arm64": 4.52.4 - "@rollup/rollup-win32-arm64-msvc": 4.52.4 - "@rollup/rollup-win32-ia32-msvc": 4.52.4 - "@rollup/rollup-win32-x64-gnu": 4.52.4 - "@rollup/rollup-win32-x64-msvc": 4.52.4 + version: 4.53.3 + resolution: "rollup@npm:4.53.3" + dependencies: + "@rollup/rollup-android-arm-eabi": 4.53.3 + "@rollup/rollup-android-arm64": 4.53.3 + "@rollup/rollup-darwin-arm64": 4.53.3 + "@rollup/rollup-darwin-x64": 4.53.3 + "@rollup/rollup-freebsd-arm64": 4.53.3 + "@rollup/rollup-freebsd-x64": 4.53.3 + "@rollup/rollup-linux-arm-gnueabihf": 4.53.3 + "@rollup/rollup-linux-arm-musleabihf": 4.53.3 + "@rollup/rollup-linux-arm64-gnu": 4.53.3 + "@rollup/rollup-linux-arm64-musl": 4.53.3 + "@rollup/rollup-linux-loong64-gnu": 4.53.3 + "@rollup/rollup-linux-ppc64-gnu": 4.53.3 + "@rollup/rollup-linux-riscv64-gnu": 4.53.3 + "@rollup/rollup-linux-riscv64-musl": 4.53.3 + "@rollup/rollup-linux-s390x-gnu": 4.53.3 + "@rollup/rollup-linux-x64-gnu": 4.53.3 + "@rollup/rollup-linux-x64-musl": 4.53.3 + "@rollup/rollup-openharmony-arm64": 4.53.3 + "@rollup/rollup-win32-arm64-msvc": 4.53.3 + "@rollup/rollup-win32-ia32-msvc": 4.53.3 + "@rollup/rollup-win32-x64-gnu": 4.53.3 + "@rollup/rollup-win32-x64-msvc": 4.53.3 "@types/estree": 1.0.8 fsevents: ~2.3.2 dependenciesMeta: @@ -19070,7 +19011,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 2918264a1dd0e1fedbe98b2c86452b00e33fcb68e3197ae7a7d2fdd195c6764d19064de7a68fbdfa460d40e65ead49224c8437112f906b9414fb9cce34b72578 + checksum: 7c5ed8f30285c731e00007726c99c6ad1f07e398d09afad53c648f32017b22b9f5d60ac99c65d60ad5334e69ffeeaa835fff88d26f21c8f1237e3d936a664056 languageName: node linkType: hard @@ -19193,8 +19134,8 @@ __metadata: linkType: hard "sass@npm:^1.54.0": - version: 1.93.2 - resolution: "sass@npm:1.93.2" + version: 1.94.2 + resolution: "sass@npm:1.94.2" dependencies: "@parcel/watcher": ^2.4.1 chokidar: ^4.0.0 @@ -19205,7 +19146,7 @@ __metadata: optional: true bin: sass: sass.js - checksum: ae466ac93687ee84a427e2e7600df3a3adde74c1ddb72ec793f6276933d96ecea373467b342bc9059f6ead71b2786041a05f12221ed0044f1e6b65223a399e8e + checksum: 75460a0d24f0ab1054bb1b88cb71cc51b592b48461ec540be9275fd4010b3b708ed734bfd337399bd8ec9c8a56942482eff6c372a3ffb0154417b6f4494522b5 languageName: node linkType: hard @@ -19489,7 +19430,7 @@ __metadata: languageName: node linkType: hard -"setprototypeof@npm:1.2.0": +"setprototypeof@npm:1.2.0, setprototypeof@npm:~1.2.0": version: 1.2.0 resolution: "setprototypeof@npm:1.2.0" checksum: be18cbbf70e7d8097c97f713a2e76edf84e87299b40d085c6bf8b65314e994cc15e2e317727342fa6996e38e1f52c59720b53fe621e2eb593a6847bf0356db89 @@ -19591,7 +19532,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": +"signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" checksum: 64c757b498cb8629ffa5f75485340594d2f8189e9b08700e69199069c8e3070fb3e255f7ab873c05dc0b3cec412aea7402e10a5990cb6a050bd33ba062a6c549 @@ -19821,12 +19762,12 @@ __metadata: languageName: node linkType: hard -"ssri@npm:^12.0.0": - version: 12.0.0 - resolution: "ssri@npm:12.0.0" +"ssri@npm:^13.0.0": + version: 13.0.0 + resolution: "ssri@npm:13.0.0" dependencies: minipass: ^7.0.3 - checksum: ef4b6b0ae47b4a69896f5f1c4375f953b9435388c053c36d27998bc3d73e046969ccde61ab659e679142971a0b08e50478a1228f62edb994105b280f17900c98 + checksum: 9705dff9e686b11f3035fb4c3d44ce690359a15a54adcd6a18951f2763f670877321178dc72c37a2b804dba3287ecaa48726dbd0cff79b2715b1cc24521b3af3 languageName: node linkType: hard @@ -19883,7 +19824,7 @@ __metadata: languageName: node linkType: hard -"statuses@npm:^2.0.1, statuses@npm:^2.0.2": +"statuses@npm:^2.0.1, statuses@npm:^2.0.2, statuses@npm:~2.0.2": version: 2.0.2 resolution: "statuses@npm:2.0.2" checksum: 6927feb50c2a75b2a4caab2c565491f7a93ad3d8dbad7b1398d52359e9243a20e2ebe35e33726dee945125ef7a515e9097d8a1b910ba2bbd818265a2f6c39879 @@ -19955,7 +19896,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": +"string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -19966,17 +19907,6 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^5.0.1, string-width@npm:^5.1.2": - version: 5.1.2 - resolution: "string-width@npm:5.1.2" - dependencies: - eastasianwidth: ^0.2.0 - emoji-regex: ^9.2.2 - strip-ansi: ^7.0.1 - checksum: 7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 - languageName: node - linkType: hard - "string.prototype.includes@npm:^2.0.1": version: 2.0.1 resolution: "string.prototype.includes@npm:2.0.1" @@ -20096,7 +20026,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": +"strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" dependencies: @@ -20191,20 +20121,20 @@ __metadata: linkType: hard "style-to-js@npm:^1.0.0": - version: 1.1.18 - resolution: "style-to-js@npm:1.1.18" + version: 1.1.21 + resolution: "style-to-js@npm:1.1.21" dependencies: - style-to-object: 1.0.11 - checksum: df544489fac603c2800685e27ea4efca0d4c3973f108b384ff98dc4093a5da00b7f89888178d7d25fb306c9efe62b2502c58fde2a00daddb14f9b72b6ae452ae + style-to-object: 1.0.14 + checksum: 01541bf726cc44ad574e4b77a65bd40aba9d652a13a9b8eeb5ca358ef52faad6c5047c591b384d4ef6a2fb315855088372ece7ec580fe73ebbbf7713935707f6 languageName: node linkType: hard -"style-to-object@npm:1.0.11": - version: 1.0.11 - resolution: "style-to-object@npm:1.0.11" +"style-to-object@npm:1.0.14": + version: 1.0.14 + resolution: "style-to-object@npm:1.0.14" dependencies: - inline-style-parser: 0.2.4 - checksum: 4a00ada833b6f23dda3948e1d603f4ab4257c7c4a197dc6b74a085cfe05b07a2167a44883c45569907eec2630116f5008180cbbf62bc301c131f138e90c575fc + inline-style-parser: 0.2.7 + checksum: 0095da3053995eb3069bba0520a52975f319eaa0d7e74e357854b805b18b622c62bfd8787cdd9d9e96b82ac0e399f6ebf90d365954bc03adc77ddcf841de5431 languageName: node linkType: hard @@ -20228,20 +20158,20 @@ __metadata: linkType: hard "sucrase@npm:^3.35.0": - version: 3.35.0 - resolution: "sucrase@npm:3.35.0" + version: 3.35.1 + resolution: "sucrase@npm:3.35.1" dependencies: "@jridgewell/gen-mapping": ^0.3.2 commander: ^4.0.0 - glob: ^10.3.10 lines-and-columns: ^1.1.6 mz: ^2.7.0 pirates: ^4.0.1 + tinyglobby: ^0.2.11 ts-interface-checker: ^0.1.9 bin: sucrase: bin/sucrase sucrase-node: bin/sucrase-node - checksum: 9fc5792a9ab8a14dcf9c47dcb704431d35c1cdff1d17d55d382a31c2e8e3063870ad32ce120a80915498486246d612e30cda44f1624d9d9a10423e1a43487ad1 + checksum: 9a3ae3900f85ede60468bdaebc07a32691d5e44c80bb008734088dcde49cd0e05ead854786d90fbb6e63ed1c50592146cb50536321212773f6d72d1c85b2a51b languageName: node linkType: hard @@ -20393,6 +20323,13 @@ __metadata: languageName: node linkType: hard +"tagged-tag@npm:^1.0.0": + version: 1.0.0 + resolution: "tagged-tag@npm:1.0.0" + checksum: e37653df3e495daa7ea7790cb161b810b00075bba2e4d6c93fb06a709e747e3ae9da11a120d0489833203926511b39e038a2affbd9d279cfb7a2f3fcccd30b5d + languageName: node + linkType: hard + "tailwindcss@npm:^3.0.2": version: 3.4.18 resolution: "tailwindcss@npm:3.4.18" @@ -20465,16 +20402,16 @@ __metadata: languageName: node linkType: hard -"tar@npm:^7.4.3": - version: 7.5.1 - resolution: "tar@npm:7.5.1" +"tar@npm:^7.5.2": + version: 7.5.2 + resolution: "tar@npm:7.5.2" dependencies: "@isaacs/fs-minipass": ^4.0.0 chownr: ^3.0.0 minipass: ^7.1.2 minizlib: ^3.1.0 yallist: ^5.0.0 - checksum: dbd55d4c3bd9e3c69aed137d9dc9fcb8f86afd103c28d97d52728ca80708f4c84b07e0a01d0bf1c8e820be84d37632325debf19f672a06e0c605c57a03636fd0 + checksum: 192559b0e7af17d57c7747592ef22c14d5eba2d9c35996320ccd20c3e2038160fe8d928fc5c08b2aa1b170c4d0a18c119441e81eae8f227ca2028d5bcaa6bf23 languageName: node linkType: hard @@ -20530,8 +20467,8 @@ __metadata: linkType: hard "terser@npm:^5.0.0, terser@npm:^5.10.0, terser@npm:^5.31.1": - version: 5.44.0 - resolution: "terser@npm:5.44.0" + version: 5.44.1 + resolution: "terser@npm:5.44.1" dependencies: "@jridgewell/source-map": ^0.3.3 acorn: ^8.15.0 @@ -20539,7 +20476,7 @@ __metadata: source-map-support: ~0.5.20 bin: terser: bin/terser - checksum: 4e1868d9662ea280dad7b49cfe61b7693187be2b529b31b1f86782db00833c03ba05f2b82fc513d928e937260f2a5fbf42a93724e86eaf55f069288f934ccdb3 + checksum: 1113c5711bb53127f9886e3c906fde8a93a665b532db9c7e36ff7bf287e032ed48ea0e5a3a1a27f6a27c3c0f934e47e7590fcd15c76b7b7bd44ad751b8a9ede4 languageName: node linkType: hard @@ -20629,13 +20566,13 @@ __metadata: linkType: hard "tinyexec@npm:^1.0.1": - version: 1.0.1 - resolution: "tinyexec@npm:1.0.1" - checksum: 40f5219abf891884863b085ebe5e8c8bf95bde802f6480f279588b355835ad1604fa01eada2afe90063b48b53cd4b0be5c37393980e23f06fd10689d92fb9586 + version: 1.0.2 + resolution: "tinyexec@npm:1.0.2" + checksum: af22de2191cc70bb782eef29bbba7cf6ac16664e550b547b0db68804f988eeb2c70e12fbb7d2d688ee994b28ba831d746e9eded98c3d10042fd3a9b8de208514 languageName: node linkType: hard -"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15": +"tinyglobby@npm:^0.2.11, tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: @@ -20680,21 +20617,21 @@ __metadata: languageName: node linkType: hard -"tldts-core@npm:^7.0.17": - version: 7.0.17 - resolution: "tldts-core@npm:7.0.17" - checksum: 8b95ad5968c3d0b9e2cf5fdf0d88b5a668093da255758a427ac16795b6a9c86a5d788dfe6803d87d95009bf6d88293b02b11b14bd88bdcb3b2354c2f346b618c +"tldts-core@npm:^7.0.19": + version: 7.0.19 + resolution: "tldts-core@npm:7.0.19" + checksum: 9270a3e23ab43995777c7dfe9b7b37b80c4c0bfe0930934a698c26e83430fbeed55a608dc35660f77cfdb87ed574a1eb3b89208ab2b6a3241a8ae9b2d5bde0ca languageName: node linkType: hard "tldts@npm:^7.0.5": - version: 7.0.17 - resolution: "tldts@npm:7.0.17" + version: 7.0.19 + resolution: "tldts@npm:7.0.19" dependencies: - tldts-core: ^7.0.17 + tldts-core: ^7.0.19 bin: tldts: bin/cli.js - checksum: 57689a9d0b632fd864328e9a7ba6dd750643bfe86bbf58cf0cd6e96b736a375b9e0430127020186ea4f5d2ad1dbc2e1c15b078aec69a04b290153bdc4bc372a9 + checksum: 24454117e4c0c6011519c3c34ff404eec03691e43ca6a5902147dff592e0dff72b37909b72bee2969466682f992bf349e442198f5acf5ffd7ea9e31b9c2ff55e languageName: node linkType: hard @@ -20714,7 +20651,7 @@ __metadata: languageName: node linkType: hard -"toidentifier@npm:1.0.1": +"toidentifier@npm:1.0.1, toidentifier@npm:~1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" checksum: 952c29e2a85d7123239b5cfdd889a0dde47ab0497f0913d70588f19c53f7e0b5327c95f4651e413c74b785147f9637b17410ac8c846d5d4a20a5a33eb6dc3a45 @@ -20993,10 +20930,12 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^4.26.1": - version: 4.41.0 - resolution: "type-fest@npm:4.41.0" - checksum: 7055c0e3eb188425d07403f1d5dc175ca4c4f093556f26871fe22041bc93d137d54bef5851afa320638ca1379106c594f5aa153caa654ac1a7f22c71588a4e80 +"type-fest@npm:^5.2.0": + version: 5.2.0 + resolution: "type-fest@npm:5.2.0" + dependencies: + tagged-tag: ^1.0.0 + checksum: 238eb0f14bd840db77c834ce5ed264d28aa79d1ed1e7b8606985f17cc01c9c1b6f266f780a132512764f81816e51b7a72a6e537188702fb985e1971ad2dcc920 languageName: node linkType: hard @@ -21213,10 +21152,10 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~7.14.0": - version: 7.14.0 - resolution: "undici-types@npm:7.14.0" - checksum: bd28cb36b33a51359f02c27b84bfe8563cdad57bdab0aa6ac605ce64d51aff49fd0aa4cb2d3b043caaa93c3ec42e96b5757df5d2d9bcc06a5f3e71899c765035 +"undici-types@npm:~7.16.0": + version: 7.16.0 + resolution: "undici-types@npm:7.16.0" + checksum: 1ef68fc6c5bad200c8b6f17de8e5bc5cfdcadc164ba8d7208cd087cfa8583d922d8316a7fd76c9a658c22b4123d3ff847429185094484fbc65377d695c905857 languageName: node linkType: hard @@ -21266,21 +21205,21 @@ __metadata: languageName: node linkType: hard -"unique-filename@npm:^4.0.0": - version: 4.0.0 - resolution: "unique-filename@npm:4.0.0" +"unique-filename@npm:^5.0.0": + version: 5.0.0 + resolution: "unique-filename@npm:5.0.0" dependencies: - unique-slug: ^5.0.0 - checksum: 6a62094fcac286b9ec39edbd1f8f64ff92383baa430af303dfed1ffda5e47a08a6b316408554abfddd9730c78b6106bef4ca4d02c1231a735ddd56ced77573df + unique-slug: ^6.0.0 + checksum: a5f67085caef74bdd2a6869a200ed5d68d171f5cc38435a836b5fd12cce4e4eb55e6a190298035c325053a5687ed7a3c96f0a91e82215fd14729769d9ac57d9b languageName: node linkType: hard -"unique-slug@npm:^5.0.0": - version: 5.0.0 - resolution: "unique-slug@npm:5.0.0" +"unique-slug@npm:^6.0.0": + version: 6.0.0 + resolution: "unique-slug@npm:6.0.0" dependencies: imurmurhash: ^0.1.4 - checksum: 222d0322bc7bbf6e45c08967863212398313ef73423f4125e075f893a02405a5ffdbaaf150f7dd1e99f8861348a486dd079186d27c5f2c60e465b7dcbb1d3e5b + checksum: ad6cf238b10292d944521714d31bc9f3ca79fa80cb7a154aad183056493f98e85de669412c6bbfe527ffa9bdeff36d3dd4d5bccaf562c794f2580ab11932b691 languageName: node linkType: hard @@ -21393,9 +21332,9 @@ __metadata: languageName: node linkType: hard -"update-browserslist-db@npm:^1.1.3": - version: 1.1.3 - resolution: "update-browserslist-db@npm:1.1.3" +"update-browserslist-db@npm:^1.1.4": + version: 1.1.4 + resolution: "update-browserslist-db@npm:1.1.4" dependencies: escalade: ^3.2.0 picocolors: ^1.1.1 @@ -21403,7 +21342,7 @@ __metadata: browserslist: ">= 4.21.0" bin: update-browserslist-db: cli.js - checksum: 7b6d8d08c34af25ee435bccac542bedcb9e57c710f3c42421615631a80aa6dd28b0a81c9d2afbef53799d482fb41453f714b8a7a0a8003e3b4ec8fb1abb819af + checksum: b757805a63d7954985753c97a48e313abd2d35f2bb10d2bffa65d73a4b81ec9e1305a7b06296819bac8a6b4db8e7be88582487fae2ad7e24731e4ee372b919a6 languageName: node linkType: hard @@ -21555,9 +21494,9 @@ __metadata: linkType: hard "validator@npm:^13.9.0": - version: 13.15.15 - resolution: "validator@npm:13.15.15" - checksum: 10c1b9215a25c31497c481cf4a3ee3e17dcf0b8a219445788e7167ed1b93b8597bbf657bd5c8ca22199b699c3ab41df902c23526cc514075c4b71bd19a13d02c + version: 13.15.23 + resolution: "validator@npm:13.15.23" + checksum: 7ac57d4c56de04cd831c997f2e06ded4862a4cf8541ac982c7d2a6a6aa10b833af20b64f1b0c6a92bae21698db44b1044f32eb2f2ce50823138ab95d4c4886e9 languageName: node linkType: hard @@ -21700,8 +21639,8 @@ __metadata: linkType: hard "vite@npm:^5.0.0": - version: 5.4.20 - resolution: "vite@npm:5.4.20" + version: 5.4.21 + resolution: "vite@npm:5.4.21" dependencies: esbuild: ^0.21.3 fsevents: ~2.3.3 @@ -21738,13 +21677,13 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 67af97e818977e92d1e55bc35d45f58d41eba6fc87f2a18435d3402adb07f4c4ec1ba838d954829f80f7784e65b65f8d147db695cfaefe45f667ee283530770d + checksum: 7177fa03cff6a382f225290c9889a0d0e944d17eab705bcba89b58558a6f7adfa1f47e469b88f42a044a0eb40c12a1bf68b3cb42abb5295d04f9d7d4dd320837 languageName: node linkType: hard "vite@npm:^5.0.0 || ^6.0.0 || ^7.0.0-0": - version: 7.1.10 - resolution: "vite@npm:7.1.10" + version: 7.2.4 + resolution: "vite@npm:7.2.4" dependencies: esbuild: ^0.25.0 fdir: ^6.5.0 @@ -21793,7 +21732,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 0157dfe5e0097b28042b83fa2d31061a05808be1bd0c0338d9690d78aa4690df3a34e8325496b66cb89b10afca1e4b318ef4a42833213eb7d20a98225596be42 + checksum: 5a8296eb9612876a5c783af1410574c524bcda5b8df5a77e4b0ff811f5caee8074cb806e775a91755128cf997dc3192de26b109c340a851050a5a5368bd48aea languageName: node linkType: hard @@ -22095,8 +22034,8 @@ __metadata: linkType: hard "webpack@npm:^5.64.4": - version: 5.102.1 - resolution: "webpack@npm:5.102.1" + version: 5.103.0 + resolution: "webpack@npm:5.103.0" dependencies: "@types/eslint-scope": ^3.7.7 "@types/estree": ^1.0.8 @@ -22115,7 +22054,7 @@ __metadata: glob-to-regexp: ^0.4.1 graceful-fs: ^4.2.11 json-parse-even-better-errors: ^2.3.1 - loader-runner: ^4.2.0 + loader-runner: ^4.3.1 mime-types: ^2.1.27 neo-async: ^2.6.2 schema-utils: ^4.3.3 @@ -22128,7 +22067,7 @@ __metadata: optional: true bin: webpack: bin/webpack.js - checksum: b43be23872e6743b47a2b9840bb3494ec512a9fa012b5e04d47d210f16462db0f741f29b3aa42d83f3859f8965a9a7990e33134e71402df19c6f78480e80c12c + checksum: dd73cb519423b4a59088e76f446f6a9c51344c53867609790e46a0bd68325033440e3dff24c1f9acd2fa985b703dc01e208b41b889e58dc5642566a9234f9260 languageName: node linkType: hard @@ -22295,14 +22234,14 @@ __metadata: languageName: node linkType: hard -"which@npm:^5.0.0": - version: 5.0.0 - resolution: "which@npm:5.0.0" +"which@npm:^6.0.0": + version: 6.0.0 + resolution: "which@npm:6.0.0" dependencies: isexe: ^3.1.1 bin: node-which: bin/which.js - checksum: 6ec99e89ba32c7e748b8a3144e64bfc74aa63e2b2eacbb61a0060ad0b961eb1a632b08fb1de067ed59b002cec3e21de18299216ebf2325ef0f78e0f121e14e90 + checksum: df19b2cd8aac94b333fa29b42e8e371a21e634a742a3b156716f7752a5afe1d73fb5d8bce9b89326f453d96879e8fe626eb421e0117eb1a3ce9fd8c97f6b7db9 languageName: node linkType: hard @@ -22530,17 +22469,6 @@ __metadata: languageName: node linkType: hard -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": - version: 7.0.0 - resolution: "wrap-ansi@npm:7.0.0" - dependencies: - ansi-styles: ^4.0.0 - string-width: ^4.1.0 - strip-ansi: ^6.0.0 - checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b - languageName: node - linkType: hard - "wrap-ansi@npm:^6.2.0": version: 6.2.0 resolution: "wrap-ansi@npm:6.2.0" @@ -22552,14 +22480,14 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^8.1.0": - version: 8.1.0 - resolution: "wrap-ansi@npm:8.1.0" +"wrap-ansi@npm:^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" dependencies: - ansi-styles: ^6.1.0 - string-width: ^5.0.1 - strip-ansi: ^7.0.1 - checksum: 371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238 + ansi-styles: ^4.0.0 + string-width: ^4.1.0 + strip-ansi: ^6.0.0 + checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b languageName: node linkType: hard @@ -22762,7 +22690,7 @@ __metadata: languageName: node linkType: hard -"yoctocolors-cjs@npm:^2.1.2": +"yoctocolors-cjs@npm:^2.1.3": version: 2.1.3 resolution: "yoctocolors-cjs@npm:2.1.3" checksum: 207df586996c3b604fa85903f81cc54676f1f372613a0c7247f0d24b1ca781905685075d06955211c4d5d4f629d7d5628464f8af0a42d286b7a8ff88e9dadcb8