diff --git a/apps/backend/.eslintrc.js b/apps/backend/.eslintrc.cjs similarity index 100% rename from apps/backend/.eslintrc.js rename to apps/backend/.eslintrc.cjs diff --git a/apps/backend/package.json b/apps/backend/package.json index a1b420a2..b8f1f40f 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -4,6 +4,8 @@ "dependencies": { "@aws-sdk/client-cognito-identity-provider": "^3.496.0", "@aws-sdk/client-s3": "^3.496.0", + "@fast-csv/format": "^5.0.5", + "archiver": "^7.0.0", "aws-sdk-mock": "^5.1.0", "axios": "^1.6.0", "body-parser": "^1.19.0", @@ -11,23 +13,26 @@ "express": "^4.17.1", "express-async-errors": "^3.1.1", "express-fileupload": "^1.2.0", + "file-type": "^21.0.0", "helmet": "^7.1.0", "join-images": "^1.1.5", "lodash": "^4.17.21", "loglevel": "^1.7.1", "mongodb-memory-server": "^7.4.0", - "mongoose": "^6.0.6", + "mongoose": "^6.13.8", "mongoose-encryption": "^2.1.0", "node-2fa": "^2.0.2", "omit-deep-lodash": "^1.1.5", "pad": "^3.2.0", "pdf2pic": "^3.1.3", + "sharp": "^0.34.3", "supertest": "^6.1.3", "twilio": "^3.71.1" }, "devDependencies": { "@3dp4me/types": "workspace:*", "@smithy/types": "^4.1.0", + "@types/archiver": "^6.0.0", "@types/body-parser": "^1.19.5", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", @@ -45,6 +50,7 @@ "webpack": "^5.89.0", "webpack-cli": "^5.1.4" }, + "exports": "./build/index.js", "jest": { "testEnvironment": "node", "testTimeout": 60000, @@ -54,13 +60,13 @@ ] }, "license": "MIT", - "main": "index.js", "scripts": { - "build": "webpack", + "build": "webpack --config webpack.prod.js", "clean": "rimraf .turbo build dist node_modules", "lint": "eslint --fix src/**/*.ts", "lint:check": "eslint src/**/*.ts", - "start": "rm -rf ./dist && tsc && doppler run -- node ./dist/src/index.js", + "start": "rm -rf ./dist && webpack --config webpack.dev.js && doppler run -- node ./build/bundle.js", "test": "cross-env S3_BUCKET_NAME=test jest --runInBand --forceExit" - } + }, + "type": "module" } diff --git a/apps/backend/scripts/export_csv.js b/apps/backend/scripts/export_csv.js deleted file mode 100644 index 0f35cbc7..00000000 --- a/apps/backend/scripts/export_csv.js +++ /dev/null @@ -1,352 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies, no-restricted-syntax, max-len, no-nested-ternary, no-case-declarations, no-param-reassign, no-await-in-loop */ -require('dotenv').config({ path: `${process.env.NODE_ENV}.env` }); -require('../utils/aws/awsSetup'); -const { exit } = require('process'); -const { - appendFileSync, - writeFile, - writeFileSync, - mkdirSync, - existsSync, - createWriteStream, -} = require('fs'); -const { join } = require('path'); - -const { createCanvas } = require('canvas'); -const ExcelJS = require('exceljs'); -const mongoose = require('mongoose'); - -const { Step } = require('../models/Metadata'); -const { Patient } = require('../models/Patient'); -const { initDB } = require('../utils/initDb'); -const { FIELDS } = require('../utils/constants'); -const { downloadFile } = require('../utils/aws/awsS3Helpers'); - -const PATIENT_INDEX_FILENAME = './patient-directory.csv'; -const PATIENT_FILE_DIR = './patients'; -const LANGUAGES = ['EN', 'AR']; - -const exportToCSV = async () => { - console.log('Exporting to CSV'); - initDB(async () => { - console.log('Connected to DB'); - await createPatientIndex(); - await createPatientFiles(); - exit(0); - }); -}; - -const createPatientFiles = async () => { - if (!existsSync(PATIENT_FILE_DIR)) mkdirSync(PATIENT_FILE_DIR); - - const patients = await Patient.find({}); - return Promise.all(patients.map(createPatientFile)); -}; - -const createPatientFile = async (patient) => { - if (!existsSync(join(PATIENT_FILE_DIR, patient.orderId))) - mkdirSync(join(PATIENT_FILE_DIR, patient.orderId)); - console.log(`Writing patient ${patient.orderId}`); - await Promise.all( - LANGUAGES.map((l) => createPatientFileInLanguage(patient, l)), - ); - console.log(`Done with patient ${patient.orderId}`); -}; - -const download = (patient, stepKey, fieldKey, fileName) => { - const localPath = join(PATIENT_FILE_DIR, patient.orderId, fileName); - const ws = createWriteStream(localPath); - - return new Promise((res, rej) => { - downloadFile(`${patient._id}/${stepKey}/${fieldKey}/${fileName}`) - .createReadStream() - .pipe(ws) - .on('end', res) - .on('error', rej); - }); -}; - -function signatureToPNG(touchpoints, outputPath) { - return new Promise((resolve, reject) => { - let minX = Infinity; - let maxX = 0; - let minY = Infinity; - let maxY = 0; - for (const point of touchpoints) { - if (point.x < minX) minX = point.x; - if (point.x > maxX) maxX = point.x; - if (point.y < minY) minY = point.y; - if (point.y > maxY) maxY = point.y; - } - - const width = maxX - minX + 10; - const height = maxY - minY + 10; - const canvas = createCanvas(width, height); - const ctx = canvas.getContext('2d'); - - // Set the background to white - ctx.fillStyle = 'white'; - ctx.fillRect(0, 0, width, height); - ctx.strokeStyle = 'black'; - - for (let i = 1; i < touchpoints.length; i++) { - const prevPoint = touchpoints[i - 1]; - const currentPoint = touchpoints[i]; - const timeDifference = currentPoint.time - prevPoint.time; - - // Adjust the line width based on the time difference - ctx.lineWidth = - timeDifference < 10 ? 1 : timeDifference < 50 ? 2 : 3; - - ctx.beginPath(); - ctx.moveTo(prevPoint.x - minX + 5, prevPoint.y - minY + 5); - ctx.lineTo(currentPoint.x - minX + 5, currentPoint.y - minY + 5); - ctx.stroke(); - } - - const buffer = canvas.toBuffer('image/png'); - writeFile(outputPath, buffer, (err) => { - if (err) { - reject(err); - return; - } - resolve(); - }); - }); -} - -function flatten(arr) { - return arr.reduce( - (flat, toFlatten) => - flat.concat( - Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten, - ), - [], - ); -} - -const decodeValue = async (patient, fieldMeta, stepData, langKey, stepKey) => { - switch (fieldMeta.fieldType) { - // There's no valuable data for dividers - case FIELDS.HEADER: - case FIELDS.DIVIDER: - return null; - - case FIELDS.MAP: - case FIELDS.ACCESS: - throw new Error(`Cannot process field ${fieldMeta.fieldType}`); - - // Recurse on field groups - case FIELDS.FIELD_GROUP: - const output = []; - - for (let i = 0; i < stepData[fieldMeta.key].length; i++) { - const data = stepData[fieldMeta.key][i]; - for (const subfield of fieldMeta.subFields) { - const key = `${fieldMeta.displayName[langKey]} ${i + 1} - ${ - subfield.displayName[langKey] - }`; - const value = await decodeValue( - patient, - subfield, - data, - langKey, - stepKey, - ); - output.push([key, value]); - } - } - - return output; - - // TODO: Take image of signature - case FIELDS.SIGNATURE: - const data = stepData[fieldMeta.key]?.signatureData; - if (!data?.length) return null; - - await signatureToPNG( - flatten(data), - join( - PATIENT_FILE_DIR, - patient.orderId, - `${fieldMeta.displayName[langKey]}.png`, - ), - ); - return 'Signature on file'; - - // TODO: Upload files to folder - case FIELDS.FILE: - case FIELDS.PHOTO: - case FIELDS.AUDIO: - const files = stepData[fieldMeta.key]; - await Promise.all( - files.map((file) => - download(patient, stepKey, fieldMeta.key, file.filename), - ), - ); - - return `${files.length} files. See patient folder for raw files.`; - - // Match radio button ID with human-friendly value - case FIELDS.RADIO_BUTTON: - const id = stepData[fieldMeta.key].toString(); - - // Question is unanswered - if (!id) return null; - - // Return the string value of the option choosen - const opt = fieldMeta.options.find((o) => o._id.toString() === id); - if (!opt) { - console.warn( - `Invalid radio button choice "${id}" on field ${fieldMeta.key}`, - ); - return null; - } - - return opt.Question[langKey]; - - // Stringify date - case FIELDS.DATE: - return stepData[fieldMeta.key].toString(); - - // For string based fields we can just return the value - case FIELDS.MULTILINE_STRING: - case FIELDS.NUMBER: - case FIELDS.PATIENT_STATUS: - case FIELDS.PHONE: - case FIELDS.STEP_STATUS: - case FIELDS.STRING: - return stepData[fieldMeta.key]; - - default: - throw new Error(`Invalid key type: ${fieldMeta.fieldType}`); - } -}; - -const autosizeColumns = (worksheet) => { - worksheet.columns.forEach((column) => { - let maxLength = 0; - column.eachCell({ includeEmpty: true }, (cell) => { - const columnLength = cell.value ? cell.value.toString().length : 10; - if (columnLength > maxLength) { - maxLength = columnLength; - } - }); - column.width = maxLength < 10 ? 10 : maxLength; - }); -}; - -const createPatientFileInLanguage = async (patient, langKey) => { - const languageString = langKey === 'EN' ? 'English' : 'Arabic'; - const workbook = new ExcelJS.Workbook(); - - const steps = await Step.find({ isDeleted: { $ne: true } }); - await Promise.all( - steps.map(async (stepMeta) => { - const sheet = workbook.addWorksheet(stepMeta.displayName[langKey]); - - const stepData = await mongoose - .model(stepMeta.key) - .findOne({ patientId: patient.id }); - - if (stepData === null) return; - - // TODO: Match up meta to data - const keyValuePairs = []; - await Promise.all( - stepMeta.fields.map(async (fieldMeta) => { - if ( - fieldMeta.isHidden || - fieldMeta.isDeleted || - [FIELDS.HEADER, FIELDS.DIVIDER].includes( - fieldMeta.fieldType, - ) - ) { - return; - } - - const key = fieldMeta.displayName[langKey]; - const value = await decodeValue( - patient, - fieldMeta, - stepData, - langKey, - stepMeta.key, - ); - if (Array.isArray(value)) keyValuePairs.push(...value); - else keyValuePairs.push([key, value]); - }), - ); - - keyValuePairs.forEach((p) => sheet.addRow(p)); - autosizeColumns(sheet); - }), - ); - - await workbook.xlsx.writeFile( - join( - PATIENT_FILE_DIR, - patient.orderId, - `${patient.orderId} (${languageString}).xlsx`, - ), - ); -}; - -const createPatientIndex = async () => { - await createEmptyFile(PATIENT_INDEX_FILENAME); - - const props = getSchemaProperties(Patient); - writeLine(PATIENT_INDEX_FILENAME, props); - - const patients = await Patient.find({}); - for (const patient of patients) { - await writePatientToFile(patient, PATIENT_INDEX_FILENAME); - } -}; - -// Function to get all top-level properties of a Mongoose Schema -function getSchemaProperties(model) { - const { schema } = model; - const { paths } = schema; - const properties = Object.keys(paths) - .filter((key) => !key.includes('.')) // This line ensures we only get top-level properties - .map((key) => key); // Modify this line if you want to manipulate the key or paths[key] in any way - - return properties; -} - -const writePatientToFile = async (patient, filename) => { - const props = getSchemaProperties(Patient); - writeLine( - filename, - props.map((p) => patient[p]), - ); -}; - -async function createEmptyFile(filePath) { - try { - writeFileSync(filePath, ''); - console.log(`File created successfully at ${filePath}`); - } catch (error) { - console.error('Error creating file:', error); - } -} - -const writeLine = (filename, items) => { - items.forEach((element) => { - writeLineItem(filename, element); - writeLineItem(filename, ','); - }); - - writeLineItem(filename, '\n'); -}; - -const writeLineItem = (filename, item) => { - if (item === undefined || item === null) - return appendFileSync(filename, ''); - if (item instanceof Date) - return appendFileSync(filename, item.toISOString()); - return appendFileSync(filename, item.toString()); -}; - -exportToCSV(); diff --git a/apps/backend/app.ts b/apps/backend/src/app.ts similarity index 80% rename from apps/backend/app.ts rename to apps/backend/src/app.ts index d92340e3..cd291d0d 100644 --- a/apps/backend/app.ts +++ b/apps/backend/src/app.ts @@ -1,6 +1,6 @@ import "express-async-errors" import path from "path" -import { router } from "./src/routes" +import { router } from "./routes" import log from "loglevel" import express, { NextFunction } from 'express' @@ -8,15 +8,15 @@ import fileUpload from "express-fileupload" import cors from "cors" import bodyParser from "body-parser" -import { requireAuthentication } from './src/middleware/authentication'; -import { initDB } from './src/utils/initDb'; +import { requireAuthentication } from './middleware/authentication'; +import { initDB } from './utils/initDb'; import { setResponseHeaders, configureHelment, -} from './src/middleware/responses' -import { logRequest } from './src/middleware/logging'; -import { ENV_TEST } from './src/utils/constants'; -import { errorHandler } from "./src/utils/errorHandler" +} from './middleware/responses' +import { logRequest } from './middleware/logging'; +import { ENV_TEST } from './utils/constants'; +import { errorHandler } from "./utils/errorHandler" import { Request, Response } from 'express'; const app = express(); diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index b3d19e11..cff990d7 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -1,9 +1,8 @@ - /** * Module dependencies. */ -import app from '../app'; +import app from './app'; import http from 'http'; /** diff --git a/apps/backend/src/routes/api/export.ts b/apps/backend/src/routes/api/export.ts new file mode 100644 index 00000000..17043ef4 --- /dev/null +++ b/apps/backend/src/routes/api/export.ts @@ -0,0 +1,37 @@ +import express, { Response } from 'express'; +import { AuthenticatedRequest } from '../../middleware/types'; +import { exportAllPatientsToZip } from '../../utils/dataextraction'; +import errorWrap from '../../utils/errorWrap'; +import { requireAdmin } from '../../middleware/authentication'; +import { queryParamToBool } from '../../utils/request'; +import { Language } from '@3dp4me/types'; +import { createReadStream, rmdirSync, rmSync } from 'fs'; + +export const router = express.Router(); + +router.get( + '/download', + requireAdmin as any, + errorWrap(async (req: AuthenticatedRequest, res: Response) => { + const includeDeleted = queryParamToBool(req.query.includeDeleted ?? 'false'); + const includeHidden = queryParamToBool(req.query.includeHidden ?? 'false'); + const language = (req.query.language as Language) || Language.EN + const zipPath = await exportAllPatientsToZip({ + language, + includeDeleted, + includeHidden, + logger: console, + }); + + // Set appropriate headers + const filename = `3dp4me_export_${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}.zip`; + res.setHeader('Content-Type', 'application/zip'); + res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); + + createReadStream(zipPath).pipe(res).on("close", () => { + rmSync(zipPath); + }); + }), +); + +export default router; \ No newline at end of file diff --git a/apps/backend/src/routes/api/index.ts b/apps/backend/src/routes/api/index.ts index 2737d2bb..326e4af3 100644 --- a/apps/backend/src/routes/api/index.ts +++ b/apps/backend/src/routes/api/index.ts @@ -1,14 +1,23 @@ import express from 'express'; +import patients from './patients'; +import steps from './steps'; +import metadata from './metadata'; +import users from './users'; +import roles from './roles'; +import publicRoutes from './public'; +import exportRoutes from './export'; + export const router = express.Router(); // Put all routes here -router.use('/patients', require('./patients')); -router.use('/stages', require('./steps')); -router.use('/metadata', require('./metadata')); -router.use('/users', require('./users')); -router.use('/roles', require('./roles')); -router.use('/public', require('./public')); +router.use('/patients', patients); +router.use('/stages', steps); +router.use('/metadata', metadata); +router.use('/users', users); +router.use('/roles', roles); +router.use('/public', publicRoutes); +router.use('/export', exportRoutes); // for export button // Disable the Twilio stuff for now // router.use('/messages', require('./messages')); diff --git a/apps/backend/src/routes/api/messages.js b/apps/backend/src/routes/api/messages.js index 4e9455a3..b496732c 100644 --- a/apps/backend/src/routes/api/messages.js +++ b/apps/backend/src/routes/api/messages.js @@ -1,5 +1,5 @@ -const express = require('express'); -const { MessagingResponse } = require('twilio').twiml; +import express from 'express'; +import { MessagingResponse } from 'twilio'; const router = express.Router(); const accountSid = process.env.ACCOUNT_SID; diff --git a/apps/backend/src/routes/api/metadata.ts b/apps/backend/src/routes/api/metadata.ts index 4bbc797d..b9d83016 100644 --- a/apps/backend/src/routes/api/metadata.ts +++ b/apps/backend/src/routes/api/metadata.ts @@ -1,8 +1,7 @@ -import { Router, Request, Response } from 'express'; - -import mongoose = require('mongoose'); -import log = require('loglevel'); +import { Router, Response } from 'express'; +import mongoose from 'mongoose'; +import log from 'loglevel'; import { requireAdmin } from '../../middleware/authentication'; import { sendResponse } from '../../utils/response'; import { @@ -112,4 +111,4 @@ router.delete( }), ); -module.exports = router; +export default router; \ No newline at end of file diff --git a/apps/backend/src/routes/api/patients.ts b/apps/backend/src/routes/api/patients.ts index d4ae5984..cdac81fc 100644 --- a/apps/backend/src/routes/api/patients.ts +++ b/apps/backend/src/routes/api/patients.ts @@ -462,4 +462,4 @@ const updatePatientStepData = async (patientId: string, StepModel: typeof mongoo return patientStepData.save(); }; -module.exports = router; +export default router; \ No newline at end of file diff --git a/apps/backend/src/routes/api/public.ts b/apps/backend/src/routes/api/public.ts index 6793bb2f..267ee99c 100644 --- a/apps/backend/src/routes/api/public.ts +++ b/apps/backend/src/routes/api/public.ts @@ -55,4 +55,4 @@ const fileFromRequest = (req: AuthenticatedRequest): Nullish => { if (!stream) throw new Error(`No read stream for ${objectKey}`); - const webstream = stream.transformToWebStream() - return Readable.fromWeb(webstream as any) + // Handle AWS SDK v3 stream properly + if (stream instanceof Readable) { + return stream; + } + + // Convert AWS SDK stream to Node.js Readable stream using the working method + const webStream = stream.transformToWebStream(); + const reader = webStream.getReader(); + + return new Readable({ + async read() { + try { + const { done, value } = await reader.read(); + if (done) { + this.push(null); // End the stream + } else { + this.push(Buffer.from(value)); + } + } catch (error) { + this.emit('error', error); + } + } + }); }; export const deleteFile = async (filePath: string) => { @@ -89,7 +114,7 @@ export const deleteFolder = async (folderName: string) => { const deleteParams = { Bucket: PATIENT_BUCKET.bucketName, - Delete: { Objects: [] as {Key: string}[] }, + Delete: { Objects: [] as { Key: string }[] }, }; // Builds a list of the files to delete @@ -117,4 +142,73 @@ function getS3(credentials: typeof S3_CREDENTIALS, region: string) { }); return s3; -} \ No newline at end of file +} + +export const fileExistsInS3 = async (s3Key: string): Promise => { + try { + await downloadFile(s3Key); + return true; + } catch (error) { + return false; + } +}; + + +export const downloadFileToPath = async ( + s3Key: string, + localPath: string +): Promise => { + const s3Stream = await downloadFile(s3Key); + const writeStream = fs.createWriteStream(localPath); + + return new Promise((resolve, reject) => { + s3Stream.pipe(writeStream) + .on('finish', () => { + resolve(); + }) + .on('error', (error) => { + reject(error); + }); + }); +}; + +/** + * Downloads a file from s3 and adds a file extension if it's missing one + */ +export const downloadFileWithTypeDetection = async ( + s3Key: string, + destinationPath: string, +) => { + await downloadFileToPath(s3Key, destinationPath); + + // If it has an extension, assume it's correct + if (hasExtension(destinationPath)) { + return + } + + // Try to detect an extension. If we can't find one, omit it + const detectedExtension = await detectFileExtension(destinationPath); + if (detectedExtension === null) { + return + } + + const pathWithExtension = `${destinationPath}.${detectedExtension}` + renameSync(destinationPath, pathWithExtension) +}; + +export const sanitizeFilename = (filename: string): string => { + const ext = path.extname(filename); + const name = path.basename(filename, ext); + const sanitizedName = name.replace(/[^a-z0-9.-]/gi, '_').toLowerCase(); + return sanitizedName + ext; +}; + +const hasExtension = (filename: string): boolean => { + return path.extname(filename).length > 0; +}; + +const detectFileExtension = async (filePath: string): Promise => { + const fileBuffer = fs.readFileSync(filePath); + const fileTypeResult = await fileTypeFromBuffer(fileBuffer); + return fileTypeResult?.ext || null; +}; \ No newline at end of file diff --git a/apps/backend/src/utils/dataextraction.ts b/apps/backend/src/utils/dataextraction.ts new file mode 100644 index 00000000..ef93f5e5 --- /dev/null +++ b/apps/backend/src/utils/dataextraction.ts @@ -0,0 +1,497 @@ +/* eslint-disable no-await-in-loop, @typescript-eslint/no-explicit-any, no-restricted-syntax */ +import { Field, FieldType, File, Language, MapPoint, Patient, Step } from '@3dp4me/types' +import { format } from '@fast-csv/format' +import archiver from 'archiver' +import { randomBytes } from 'crypto' +import fs, { createWriteStream, mkdirSync, mkdtempSync, rmSync } from 'fs' +import mongoose from 'mongoose' +import { tmpdir } from 'os' +import path, { join } from 'path' + +import { StepModel } from '../models/Metadata' +import { PatientModel } from '../models/Patient' +import { downloadFileWithTypeDetection, fileExistsInS3, sanitizeFilename } from './aws/awsS3Helpers' + +const IGNORED_FIELD_TYPES = [ + FieldType.FILE, + FieldType.AUDIO, + FieldType.PHOTO, + FieldType.SIGNATURE, + FieldType.DIVIDER, + FieldType.HEADER, +] + +const MEDIA_FIELD_TYPES = [ + FieldType.FILE, + FieldType.AUDIO, + FieldType.PHOTO, + // FieldType.SIGNATURE, (not media, stored as an array of points on a canvas in mongo. generate an image of this signature and save it) +] + +// Generic logging interface so that we can eventually send the progress over websocket or something similar +interface Logger { + debug: LevelLogger + info: LevelLogger + error: LevelLogger +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type LevelLogger = (message: string, ...args: any[]) => void + +interface ExportOptions { + includeDeleted: boolean + includeHidden: boolean + logger: Logger + language: Language +} + +const PATIENT_ID_TO_HEADER: Partial, string>> = { + dateCreated: 'Date Created', + orderId: 'Order ID', + lastEdited: 'Last Edited', + lastEditedBy: 'Last Edited By', + status: 'Status', + phoneNumber: 'Phone Number', + orderYear: 'Order Year', + firstName: 'First Name', + fathersName: "Father's Name", + grandfathersName: "Grandfather's Name", + familyName: 'Family Name', +} + +function createCsvWriteStream(filepath: string) { + mkdirSync(path.dirname(filepath), { recursive: true }) + const destination = createWriteStream(filepath, { flags: 'w+', flush: true }) + const stream = format({ headers: true, quote: '"', quoteColumns: true, quoteHeaders: true }) + stream.pipe(destination) + return stream +} + +async function writePatientCsv(logger: Logger, filepath: string) { + const stream = createCsvWriteStream(filepath) + const patients = await PatientModel.find() + patients.forEach((p) => { + const patient = p.toObject() + const row = patientToCsvRow(logger, patient) + stream.write(row) + }) + + logger.info(`Generated patients.csv with ${patients.length} records`) + stream.end() +} + +function patientToCsvRow(logger: Logger, patient: Patient): Record { + const csvRow: Record = {} + for (const [key, value] of Object.entries(patient)) { + const header = PATIENT_ID_TO_HEADER[key as keyof Patient] + if (!header) { + logger.debug(`Skipping unknown patient key: ${key}`) + continue + } + + csvRow[header] = value + } + + return csvRow +} + +async function getSteps(options: ExportOptions): Promise { + // Build query filter based on options + const stepFilter: any = {} + if (!options.includeDeleted) { + stepFilter.isDeleted = { $ne: true } + } + + if (!options.includeHidden) { + stepFilter.isHidden = { $ne: true } + } + + // Get step definitions based on filter + const stepDefinitions = await StepModel.find(stepFilter).lean() + options.logger.debug(`Found ${stepDefinitions.length} step definitions`) + return stepDefinitions +} + +async function writeStepCsvs(directoryLocation: string, options: ExportOptions) { + const { logger } = options + logger.info('=== STEP 2: Generating Step CSVs ===') + fs.mkdirSync(directoryLocation, { recursive: true }) + + // Get step definitions using the global getSteps function + const steps = await getSteps(options) + const patients = await PatientModel.find() + logger.info(`Found ${steps.length} steps and ${patients.length} patients`) + const stepPromises = steps.map(async (step) => + writeStepToCSV(directoryLocation, step, patients, options) + ) + await Promise.all(stepPromises) +} + +function shouldIgnoreField(field: Field, options: ExportOptions) { + const { includeDeleted, includeHidden } = options + if (IGNORED_FIELD_TYPES.includes(field.fieldType)) return true + if (!includeHidden && field.isHidden) return true + if (!includeDeleted && field.isDeleted) return true + return false +} + +function getStepModel(stepKey: string): mongoose.Model | null { + try { + return mongoose.model(stepKey) + } catch (error) { + return null + } +} + +async function writeStepToCSV( + directoryLocation: string, + step: Step, + patients: Patient[], + options: ExportOptions +) { + const stepKey = step.key + options.logger.info(`Processing step: ${stepKey}`) + + if (!getStepModel(stepKey)) { + options.logger.info(`No model found for step ${stepKey}, skipping`) + return + } + + const fieldsToWrite = step.fields.filter((field: Field) => !shouldIgnoreField(field, options)) + + const regularFields = fieldsToWrite.filter( + (field: Field) => field.fieldType !== FieldType.FIELD_GROUP + ) + + const fieldGroups = fieldsToWrite.filter( + (field: Field) => + field.fieldType === FieldType.FIELD_GROUP && Array.isArray(field.subFields) + ) + + options.logger.info(`Writing ${regularFields.length} regular fields to ${step.key}.csv`) + await writeRegularFieldsToCSV(directoryLocation, step, patients, regularFields, options) + + options.logger.info(`Writing ${fieldGroups.length} field groups to ${step.key}.csv`) + await writeFieldGroupsToCSV(directoryLocation, step, patients, fieldGroups, options) +} + +/** + * Writes all patients in this step to a single CSV file + */ +async function writeRegularFieldsToCSV( + directoryLocation: string, + stepMeta: Step, + patients: Patient[], + fields: Field[], + options: ExportOptions +) { + const StepDataModel = getStepModel(stepMeta.key)! + const patientPromises = patients.map(async (patient) => { + const row: Record = { + 'Order ID': patient.orderId, + } + + // Process each field in the step definition + const stepDoc = await StepDataModel.findOne({ patientId: patient._id }) + if (!stepDoc) { + return null + } + + for (const field of fields) { + const fieldName = getFieldName(field, options) + row[fieldName] = fieldToString(stepDoc?.[field.key], field, options) + } + + return row + }) + + let rows = await Promise.all(patientPromises) + rows = rows.filter((r) => r !== null) + if (rows.length === 0) { + options.logger.debug(`No records found for step ${stepMeta.key}, skipping`) + return + } + + const stream = createCsvWriteStream(path.join(directoryLocation, `${stepMeta.key}.csv`)) + rows.forEach((r) => stream.write(r)) + stream.end() + options.logger.debug(`Wrote ${patientPromises.length} records to ${stepMeta.key}.csv`) +} + +function fieldGroupToRows( + fieldGroup: Field, + stepDoc: Record | null, + options: ExportOptions +): Record[] { + const values = stepDoc?.[fieldGroup.key] + if (!Array.isArray(values)) return [] + + const rows: Record[] = [] + for (const [index, fieldGroupEntry] of values.entries()) { + const fieldGroupName = getFieldName(fieldGroup, options) + const row: Record = { + [fieldGroupName]: `Entry ${index + 1}`, + } + for (const field of fieldGroup.subFields) { + if (shouldIgnoreField(field, options)) continue + const fieldName = getFieldName(field, options) + row[fieldName] = fieldToString(fieldGroupEntry?.[field?.key], field, options) + } + + rows.push(row) + } + + return rows +} + +async function writeFieldGroupsToCSV( + directoryLocation: string, + stepMeta: Step, + patients: Patient[], + fieldGroups: Field[], + options: ExportOptions +) { + const StepDataModel = getStepModel(stepMeta.key)! + const patientPromises = patients.map(async (patient) => { + for (const fieldGroup of fieldGroups) { + const stepDoc = await StepDataModel.findOne({ patientId: patient._id }) + const rows = fieldGroupToRows(fieldGroup, stepDoc, options) + if (rows.length === 0) continue + const csvFileName = `${path.join( + directoryLocation, + patient.orderId, + stepMeta.key, + fieldGroup.key + )}.csv` + const stream = createCsvWriteStream(csvFileName) + rows.forEach((r) => stream.write(r)) + stream.end() + } + }) + + await Promise.all(patientPromises) + options.logger.debug(`Wrote ${patientPromises.length} records for field groups`) +} + +function getFieldName(field: Field, options: ExportOptions): string { + return field.displayName[options.language] || field.key +} + +function fieldToString(value: any, field: Field, options: ExportOptions): string { + if (!value) return '' + + switch (field.fieldType) { + case FieldType.STRING: + case FieldType.MULTILINE_STRING: + case FieldType.PHONE: + return value as string + case FieldType.NUMBER: + return value.toString() + case FieldType.DATE: + return new Date(value).toISOString() + case FieldType.MULTI_SELECT: + case FieldType.TAGS: + // Should always be an array. Fallback to JSON stringify + if (!Array.isArray(value)) { + options.logger.error( + `Expected ${field.key} (type ${ + field.fieldType + }) to be an array, got ${typeof value}` + ) + return JSON.stringify(value) + } + + // eslint-disable-next-line no-case-declarations + const selectedValues = value.map((val) => + getFieldOptionText(field, val, options.language) + ) + return selectedValues.join(', ') + case FieldType.RADIO_BUTTON: + return getFieldOptionText(field, value, options.language) + case FieldType.MAP: + // Format MAP data as "lat,lng" + if (!isMapPoint(value)) { + options.logger.error( + `Expected ${field.key} (type ${ + field.fieldType + }) to be a MapPoint, got ${typeof value}` + ) + return JSON.stringify(value) + } + + return `${value.latitude},${value.longitude}` + default: + return `Unknown field type: ${field.fieldType}` + } +} + +function isMapPoint(value: any): value is MapPoint { + return typeof value === 'object' && 'latitude' in value && 'longitude' in value +} + +async function writeMediaFiles(directoryLocation: string, options: ExportOptions) { + const { includeDeleted, includeHidden, logger } = options + logger.info('\n=== STEP 3: Exporting Media Files ===') + logger.debug(`Options: includeDeleted=${includeDeleted}, includeHidden=${includeHidden}`) + + const stepDefinitions = await getSteps(options) + const patients = await PatientModel.find() + logger.info(`Found ${patients.length} patients`) + + const stepPromises = stepDefinitions.map(async (step) => + writeMediaFilesForStep(directoryLocation, step, patients, options) + ) + + await Promise.all(stepPromises) + logger.info(`Media export complete`) +} + +async function writeMediaFilesForStep( + directoryLocation: string, + step: Step, + patients: Patient[], + options: ExportOptions +) { + const stepKey = step.key + options.logger.info(`Processing step: ${stepKey}`) + + const StepDataModel = getStepModel(stepKey) + if (!StepDataModel) { + options.logger.info(`No model found for step ${stepKey}, skipping`) + return + } + + const patientPromises = patients.map(async (patient) => + writeMediaFilesForPatient(directoryLocation, StepDataModel, step, patient, options) + ) + + await Promise.all(patientPromises) +} + +async function writeMediaFilesForPatient( + directoryLocation: string, + StepDataModel: mongoose.Model, + step: Step, + patient: Patient, + options: ExportOptions +) { + const stepDoc = await StepDataModel.findOne({ patientId: patient._id }) + if (!stepDoc) return + + const patientDir = path.join(directoryLocation, patient.orderId) + const stepDir = path.join(patientDir, step.key) + + // Process regular fields + let numFilesDownloaded = 0 + const filePromises = step.fields.map(async (field) => { + if (!options.includeHidden && field.isHidden) return + if (!options.includeDeleted && field.isDeleted) return + if (!MEDIA_FIELD_TYPES.includes(field.fieldType)) return + + const fileData = stepDoc?.[field.key] as File[] | null + if (!fileData) return + + // Handle array of files + for (const file of fileData) { + if (file && file.filename) { + const s3Key = `${patient._id}/${step.key}/${field.key}/${file.filename}` + const fileExists = await fileExistsInS3(s3Key) + if (!fileExists) continue + + fs.mkdirSync(stepDir, { recursive: true }) + const sanitizedFilename = sanitizeFilename(file.filename) + const filePath = path.join(stepDir, sanitizedFilename) + + try { + await downloadFileWithTypeDetection(s3Key, filePath) + options.logger.debug(`Downloaded file ${s3Key}`) + numFilesDownloaded++ + } catch (error) { + options.logger.error(`Failed to download file ${s3Key}: ${error}`) + } + } + } + }) + + await Promise.all(filePromises) + options.logger.info( + `Downloaded ${numFilesDownloaded} files for patient ${patient.orderId} in step ${step.key}` + ) +} + +// Helper function to resolve field option IDs to human-readable text +function getFieldOptionText(field: Field, value: any, lang: Language): string { + if (!field.options || !Array.isArray(field.options) || !value) { + return '' + } + + // Find the option that matches the value (ID) + const valStr = value.toString() + const matchingOption = field.options.find((opt) => opt._id?.toString() === valStr) + if (matchingOption?.Question?.[lang]) { + return matchingOption.Question[lang] + } + + return '' +} + +export async function exportAllPatientsToZip(options: ExportOptions): Promise { + const { includeDeleted = false, includeHidden = false, logger = console } = options + + logger.debug('Export Configuration:', { includeDeleted, includeHidden }) + const destination = mkdtempSync(join(tmpdir(), '3dp4me-export-')) + logger.debug(`Exporting to ${destination}`) + + try { + logger.info('Generating CSV Files') + await writePatientCsv(logger, join(destination, 'patients.csv')) + await writeStepCsvs(destination, options) + + logger.info('Downloading Media Files') + await writeMediaFiles(destination, options) + + logger.info('Creating ZIP File') + const zipPath = await zipDirectory(destination, logger) + + logger.info('Doing Cleanup') + rmSync(destination, { recursive: true }) + + logger.info('Export complete') + return zipPath + } catch (error) { + logger.error('Error during export process:', error) + throw error + } +} + +// New function to create a zip stream instead of a file +async function zipDirectory(directory: string, logger: Logger): Promise { + logger.info('\n=== STEP 4: Creating ZIP Stream ===') + const zipPath = join(tmpdir(), `3dp4me-zip-${randomBytes(64).toString('hex')}.zip`) + const archive = archiver('zip', { + zlib: { level: 9 }, // Maximum compression + }) + + // Write zip to file + const outputStream = fs.createWriteStream(zipPath, { autoClose: true }) + archive.pipe(outputStream) + + // Handle archiver events + archive.on('warning', (err) => { + if (err.code === 'ENOENT') { + logger.info('Archive warning - file not found:', err.message) + } else { + logger.error('Archive warning (treating as error):', err) + throw err + } + }) + + archive.on('error', (err) => { + logger.error('Error creating ZIP archive:', err) + throw err + }) + + archive.directory(directory, false) + await archive.finalize() + return zipPath +} diff --git a/apps/backend/src/utils/initDb.ts b/apps/backend/src/utils/initDb.ts index 07a7c82f..8e55add6 100644 --- a/apps/backend/src/utils/initDb.ts +++ b/apps/backend/src/utils/initDb.ts @@ -2,6 +2,7 @@ import { Field, FieldType, PatientTagsField, + PatientTagSyria, ReservedStep, RootStep, RootStepFieldKeys, @@ -16,7 +17,6 @@ import encrypt from 'mongoose-encryption' import { StepModel } from '../models/Metadata' import { fileSchema } from '../schemas/fileSchema' import { signatureSchema } from '../schemas/signatureSchema' -import { PatientTagSyria } from '@3dp4me/types'; /** * Initalizes and connects to the DB. Should be called at app startup. @@ -47,17 +47,17 @@ const clearModels = async () => { // Migrations for root step const initReservedSteps = async () => { - log.info("Initializing the reserved step") + log.info('Initializing the reserved step') const rootStep = await StepModel.findOne({ key: ReservedStep.Root }).lean() if (!rootStep) { - log.info("Creating the reserved step") + log.info('Creating the reserved step') return StepModel.create(RootStep) } // Older version missing the tag field const tagField = rootStep.fields.find((f) => f.key === RootStepFieldKeys.Tags) if (!tagField) { - log.info("Tags is missing from reserved step, adding it") + log.info('Tags is missing from reserved step, adding it') return StepModel.updateOne( { key: ReservedStep.Root }, { $push: { fields: PatientTagsField } } @@ -67,17 +67,17 @@ const initReservedSteps = async () => { // Older version missing the syria option const syriaOption = tagField.options.find((o) => o.Question.EN === PatientTagSyria.Question.EN) if (!syriaOption) { - log.info("Syria is missing from tag options, adding it") + log.info('Syria is missing from tag options, adding it') return StepModel.updateOne( - { + { key: ReservedStep.Root, - "fields.key": RootStepFieldKeys.Tags + 'fields.key': RootStepFieldKeys.Tags, }, - { $push: { "fields.$.options": PatientTagSyria } } + { $push: { 'fields.$.options': PatientTagSyria } } ) } - log.info("Reserved step is up to date") + log.info('Reserved step is up to date') return null } diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index d422b029..88d8e945 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "module": "commonjs", + "module": "ESNext", "esModuleInterop": true, "allowSyntheticDefaultImports": true, "target": "ESNext", diff --git a/apps/backend/webpack.config.js b/apps/backend/webpack.config.js deleted file mode 100644 index 9b089e4a..00000000 --- a/apps/backend/webpack.config.js +++ /dev/null @@ -1,27 +0,0 @@ -module.exports = { - entry: "./src/index.ts", - target: 'node', - module: { - rules: [ - { - test: /\.tsx?$/, - use: 'ts-loader', - exclude: /node_modules/, - }, - { - test: /\.node$/, - use: 'node-loader', - }, - ], - }, - node: { - __dirname: false, - }, - resolve: { - extensions: ['.tsx', '.ts', '.js', '.node'], - }, - output: { - filename: 'bundle.js', - path: __dirname + '/build', - }, -}; diff --git a/apps/backend/webpack.dev.js b/apps/backend/webpack.dev.js new file mode 100644 index 00000000..981f957a --- /dev/null +++ b/apps/backend/webpack.dev.js @@ -0,0 +1,43 @@ +// webpack.config.js +import { ExpirationStatus } from '@aws-sdk/client-s3'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export default { + entry: "./src/index.ts", + mode: 'development', + target: 'node', + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + { + test: /\.node$/, + use: 'node-loader', + }, + ], + }, + node: { + __dirname: true, + }, + resolve: { + extensions: ['.tsx', '.ts', '.js', '.node'], + }, + output: { + path: path.resolve(__dirname, 'build'), + filename: 'bundle.js', + module: true, + }, + experiments: { + outputModule: true, + }, + externals: { + sharp: 'module sharp', + } +}; diff --git a/apps/backend/webpack.prod.js b/apps/backend/webpack.prod.js new file mode 100644 index 00000000..7d5949d3 --- /dev/null +++ b/apps/backend/webpack.prod.js @@ -0,0 +1,43 @@ +// webpack.config.js +import { ExpirationStatus } from '@aws-sdk/client-s3'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export default { + entry: "./src/index.ts", + mode: 'production', + target: 'node', + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + { + test: /\.node$/, + use: 'node-loader', + }, + ], + }, + node: { + __dirname: true, + }, + resolve: { + extensions: ['.tsx', '.ts', '.js', '.node'], + }, + output: { + path: path.resolve(__dirname, 'build'), + filename: 'bundle.js', + module: true, + }, + experiments: { + outputModule: true, + }, + externals: { + sharp: 'module sharp', + } +}; diff --git a/apps/frontend/src/api/api.ts b/apps/frontend/src/api/api.ts index a394f431..8e76abec 100644 --- a/apps/frontend/src/api/api.ts +++ b/apps/frontend/src/api/api.ts @@ -321,3 +321,12 @@ export const getSelf = async (): Promise> => { return res.data } + +export const downloadAllPatientData = async (includeDeleted: boolean, includeHidden: boolean) => { + const res = await instance.get('/export/download', { + params: { includeDeleted, includeHidden }, + responseType: 'blob', + }) + + fileDownload(res.data, '3dp4me_export.zip') +} diff --git a/apps/frontend/src/components/ExportModal/ExportModal.tsx b/apps/frontend/src/components/ExportModal/ExportModal.tsx new file mode 100644 index 00000000..018df71a --- /dev/null +++ b/apps/frontend/src/components/ExportModal/ExportModal.tsx @@ -0,0 +1,95 @@ +import { + Button, + Checkbox, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, +} from '@mui/material' +import React, { useState } from 'react' + +import { downloadAllPatientData } from '../../api/api' +import { useTranslations } from '../../hooks/useTranslations' + +interface ExportButtonProps { + isOpen: boolean + onClose: () => void + onExportComplete?: () => void + onExportError?: (error: Error) => void +} + +const ExportModal: React.FC = ({ + onExportComplete, + onExportError, + isOpen, + onClose, +}) => { + const translations = useTranslations()[0] + const [includeDeleted, setIncludeDeleted] = useState(false) + const [includeHidden, setIncludeHidden] = useState(false) + const [loading, setLoading] = useState(false) + + const handleDownload = async () => { + setLoading(true) + try { + await downloadAllPatientData(includeDeleted, includeHidden) + onExportComplete?.() + } catch (error) { + console.error('Export failed:', error) + onExportError?.(error as Error) + } finally { + setLoading(false) + onClose() + } + } + + return ( + <> + + + {translations.exportOptionsTitle} + + + setIncludeDeleted(e.target.checked)} + disabled={loading} + /> + } + label={translations.exportIncludeDeleted} + /> + setIncludeHidden(e.target.checked)} + disabled={loading} + /> + } + label={translations.exportIncludeHidden} + /> + + + + + + + + ) +} + +export default ExportModal diff --git a/apps/frontend/src/components/Fields/SignatureField.tsx b/apps/frontend/src/components/Fields/SignatureField.tsx index 9baa2dec..25b2fffe 100644 --- a/apps/frontend/src/components/Fields/SignatureField.tsx +++ b/apps/frontend/src/components/Fields/SignatureField.tsx @@ -42,7 +42,7 @@ const SignatureField = ({ onChange(`${fieldId}.signatureCanvasWidth`, data.width) onChange(`${fieldId}.signatureCanvasHeight`, data.height) - if (!!documentURL) { + if (documentURL) { onChange(`${fieldId}.documentURL.EN`, documentURL.EN) onChange(`${fieldId}.documentURL.AR`, documentURL.AR) } diff --git a/apps/frontend/src/components/Navbar/Navbar.tsx b/apps/frontend/src/components/Navbar/Navbar.tsx index c0030c56..bb1059a1 100644 --- a/apps/frontend/src/components/Navbar/Navbar.tsx +++ b/apps/frontend/src/components/Navbar/Navbar.tsx @@ -11,6 +11,7 @@ import { useTranslations } from '../../hooks/useTranslations' import { Context } from '../../store/Store' import { Routes } from '../../utils/constants' import AccountDropdown from '../AccountDropdown/AccountDropdown' +import ExportModal from '../ExportModal/ExportModal' export interface NavbarProps { username: string @@ -75,6 +76,7 @@ const Navbar = ({ username, userEmail }: NavbarProps) => { const [translations, selectedLang] = useTranslations() const [activeRoute, setActiveRoute] = useState(window.location.pathname) const [anchorEl, setAnchorEl] = useState>(null) + const [isExportModalOpen, setIsExportModalOpen] = useState(false) const navTranslations = translations.components.navbar const handleAccountClick: MouseEventHandler = (e) => { @@ -115,12 +117,19 @@ const Navbar = ({ username, userEmail }: NavbarProps) => { navTranslations.dashboardManagement.navTitle, Routes.DASHBOARD_MANAGEMENT ), + renderExport(), ]) } return links } + const renderExport = () => ( + setIsExportModalOpen(true)} to={'#'}> + {translations.exportPatientData} + + ) + return ( @@ -141,6 +150,12 @@ const Navbar = ({ username, userEmail }: NavbarProps) => { {renderLinks()} + setIsExportModalOpen(false)} + onExportError={(error) => alert(`Export failed: ${error.message}`)} + /> +
diff --git a/apps/frontend/src/pages/PatientDetail/PatientDetail.tsx b/apps/frontend/src/pages/PatientDetail/PatientDetail.tsx index 8858c890..2400eb66 100644 --- a/apps/frontend/src/pages/PatientDetail/PatientDetail.tsx +++ b/apps/frontend/src/pages/PatientDetail/PatientDetail.tsx @@ -206,22 +206,26 @@ const PatientDetail = () => { onUploadProfilePicture={onUploadProfilePicture} /> - setManagePatientModalOpen(true)} - /> + setManagePatientModalOpen(true)} + /> -
- - {generateStepContent()} -
+
+ + {generateStepContent()} +
) } -export default PatientDetail \ No newline at end of file +export default PatientDetail diff --git a/apps/frontend/src/translations.json b/apps/frontend/src/translations.json index 96492774..18fd4a6b 100644 --- a/apps/frontend/src/translations.json +++ b/apps/frontend/src/translations.json @@ -1,5 +1,12 @@ { "EN": { + "exportPatientData": "Export Patient Data", + "exportOptionsTitle": "Export Options", + "exportIncludeDeleted": "Include Deleted Steps", + "exportIncludeHidden": "Include Hidden Fields", + "exportAsZip": "Export as ZIP", + "cancel": "Cancel", + "exporting": "Exporting...", "patient2FA": { "patientLogin": "Patient Login", "patientID": "Patient ID:", @@ -265,6 +272,13 @@ } }, "AR": { + "exportPatientData": "تصدير بيانات المرضى", + "exportOptionsTitle": "خيارات التصدير", + "exportIncludeDeleted": "تضمين الخطوات المحذوفة", + "exportIncludeHidden": "تضمين الحقول المخفية", + "exportAsZip": "تصدير كملف ZIP", + "cancel": "يلغي", + "exporting": "جارٍ التصدير...", "patient2FA": { "patientLogin": "تسجيل دخول المريض", "patientID": "هوية المريض:", diff --git a/packages/types/src/models/field.ts b/packages/types/src/models/field.ts index f4bc5c8a..018a2539 100644 --- a/packages/types/src/models/field.ts +++ b/packages/types/src/models/field.ts @@ -1,5 +1,4 @@ import { Nullish, Unsaved } from '../utils' - import { File } from './file' import { MapPoint } from './map' import { Signature } from './signature' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 072e9bbc..78eee09b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,12 @@ importers: '@aws-sdk/client-s3': specifier: ^3.496.0 version: 3.735.0 + '@fast-csv/format': + specifier: ^5.0.5 + version: 5.0.5 + archiver: + specifier: ^7.0.0 + version: 7.0.1 aws-sdk-mock: specifier: ^5.1.0 version: 5.9.0 @@ -53,12 +59,15 @@ importers: express-fileupload: specifier: ^1.2.0 version: 1.5.1 + file-type: + specifier: ^21.0.0 + version: 21.0.0 helmet: specifier: ^7.1.0 version: 7.2.0 join-images: specifier: ^1.1.5 - version: 1.1.5(sharp@0.32.6) + version: 1.1.5(sharp@0.34.3) lodash: specifier: ^4.17.21 version: 4.17.21 @@ -69,7 +78,7 @@ importers: specifier: ^7.4.0 version: 7.6.3 mongoose: - specifier: ^6.0.6 + specifier: ^6.13.8 version: 6.13.8 mongoose-encryption: specifier: ^2.1.0 @@ -86,6 +95,9 @@ importers: pdf2pic: specifier: ^3.1.3 version: 3.1.3 + sharp: + specifier: ^0.34.3 + version: 0.34.3 supertest: specifier: ^6.1.3 version: 6.3.4 @@ -99,6 +111,9 @@ importers: '@smithy/types': specifier: ^4.1.0 version: 4.1.0 + '@types/archiver': + specifier: ^6.0.0 + version: 6.0.3 '@types/body-parser': specifier: ^1.19.5 version: 1.19.5 @@ -1784,6 +1799,9 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@borewit/text-codec@0.1.1': + resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} + '@changesets/apply-release-plan@7.0.8': resolution: {integrity: sha512-qjMUj4DYQ1Z6qHawsn7S71SujrExJ+nceyKKyI9iB+M5p9lCL55afuEd6uLBPRpLGWQwkwvWegDHtwHJb1UjpA==} @@ -1942,9 +1960,13 @@ packages: '@effect/schema@0.69.0': resolution: {integrity: sha512-dqVnriWqM8TT8d+5vgg1pH4ZOXfs7tQBVAY5N7PALzIOlZamsBKQCdwvMMqremjSkKITckF8/cIj1eQZHQeg7Q==} + deprecated: this package has been merged into the main effect package peerDependencies: effect: ^3.5.7 + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -2029,6 +2051,9 @@ packages: '@fast-csv/format@4.3.5': resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==} + '@fast-csv/format@5.0.5': + resolution: {integrity: sha512-0P9SJXXnqKdmuWlLaTelqbrfdgN37Mvrb369J6eNmqL41IEIZQmV4sNM4GgAK2Dz3aH04J0HKGDMJFkYObThTw==} + '@fast-csv/parse@4.3.6': resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==} @@ -2045,6 +2070,128 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead + '@img/sharp-darwin-arm64@0.34.3': + resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.3': + resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.0': + resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.0': + resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.0': + resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.0': + resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.0': + resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.0': + resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.0': + resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.3': + resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.3': + resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.3': + resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.3': + resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.3': + resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.3': + resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.3': + resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.3': + resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.3': + resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.3': + resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.3': + resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -3014,6 +3161,13 @@ packages: peerDependencies: react: ^18 || ^19 + '@tokenizer/inflate@0.2.7': + resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@tootallnate/once@1.1.2': resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} engines: {node: '>= 6'} @@ -3034,6 +3188,9 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/archiver@6.0.3': + resolution: {integrity: sha512-a6wUll6k3zX6qs5KlxIggs1P1JcYJaTCx2gnlr+f0S1yd2DoaEwoIK10HmBaLnZwWneBz+JBm0dwcZu0zECBcQ==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -3228,6 +3385,9 @@ packages: '@types/react@18.3.18': resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==} + '@types/readdir-glob@1.1.5': + resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} + '@types/resolve@1.17.1': resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} @@ -3643,10 +3803,18 @@ packages: resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} engines: {node: '>= 10'} + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + archiver@5.3.2: resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} engines: {node: '>= 10'} + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -3887,28 +4055,6 @@ packages: bare-events@2.5.4: resolution: {integrity: sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==} - bare-fs@4.0.1: - resolution: {integrity: sha512-ilQs4fm/l9eMfWY2dY0WCIUplSUp7U0CT1vrqMg1MUdeZl4fypu5UP0XcDBK5WBQPJAKP1b7XEodISmekH/CEg==} - engines: {bare: '>=1.7.0'} - - bare-os@3.4.0: - resolution: {integrity: sha512-9Ous7UlnKbe3fMi7Y+qh0DwAup6A1JkYgPnjvMDNOlmnxNRQvQ/7Nst+OnUQKzk0iAT0m9BisbDVp9gCv8+ETA==} - engines: {bare: '>=1.6.0'} - - bare-path@3.0.0: - resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} - - bare-stream@2.6.4: - resolution: {integrity: sha512-G6i3A74FjNq4nVrrSTUz5h3vgXzBJnjmWAVlBWaZETkgu+LgKd7AiyOml3EDJY1AHlIbBHKDXE+TUT53Ff8OaA==} - peerDependencies: - bare-buffer: '*' - bare-events: '*' - peerDependenciesMeta: - bare-buffer: - optional: true - bare-events: - optional: true - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -4017,6 +4163,7 @@ packages: bson@6.10.1: resolution: {integrity: sha512-P92xmHDQjSKPLHqFxefqMxASNq/aWJMEZugpCjf+AF/pgcUpMMQCg7t7+ewko0/u8AapvF3luf/FoehddEK+sA==} engines: {node: '>=16.20.1'} + deprecated: a critical bug affecting only useBigInt64=true deserialization usage is fixed in bson@6.10.3 btoa@1.2.1: resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==} @@ -4032,6 +4179,10 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -4051,6 +4202,9 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + buffers@0.1.1: resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} engines: {node: '>=0.2.0'} @@ -4175,9 +4329,6 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chownr@1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - chrome-launcher@0.15.2: resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==} engines: {node: '>=12.13.0'} @@ -4325,6 +4476,10 @@ packages: resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} engines: {node: '>= 10'} + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} @@ -4436,6 +4591,10 @@ packages: resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} engines: {node: '>= 10'} + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -4677,10 +4836,6 @@ packages: resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} engines: {node: '>=4'} - decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - decompress-tar@4.1.1: resolution: {integrity: sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==} engines: {node: '>=4'} @@ -4704,10 +4859,6 @@ packages: dedent@0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -4766,8 +4917,8 @@ packages: engines: {node: '>=0.10'} hasBin: true - detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} detect-newline@3.1.0: @@ -5355,10 +5506,6 @@ packages: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} - expand-template@2.0.3: - resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} - engines: {node: '>=6'} - expect@27.5.1: resolution: {integrity: sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -5463,6 +5610,9 @@ packages: fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -5481,6 +5631,10 @@ packages: resolution: {integrity: sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg==} engines: {node: '>=8'} + file-type@21.0.0: + resolution: {integrity: sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==} + engines: {node: '>=20'} + file-type@3.9.0: resolution: {integrity: sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==} engines: {node: '>=0.10.0'} @@ -5626,6 +5780,7 @@ packages: formidable@2.1.2: resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} + deprecated: 'ACTION REQUIRED: SWITCH TO v3 - v1 and v2 are VULNERABLE! v1 is DEPRECATED FOR OVER 2 YEARS! Use formidable@latest or try formidable-mini for fresh projects' forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} @@ -5759,9 +5914,6 @@ packages: git-hooks-list@1.0.3: resolution: {integrity: sha512-Y7wLWcrLUXwk2noSka166byGCvhMtDRpgHdzCno1UQv/n/Hegp++a2xBWJL1lJarnKD3SWaljD+0z1ztqxuKyQ==} - github-from-package@0.0.0: - resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -5829,6 +5981,7 @@ packages: gm@1.25.0: resolution: {integrity: sha512-4kKdWXTtgQ4biIo7hZA396HT062nDVVHPjQcurNZ3o/voYN+o5FUC5kOwuORbpExp3XbTJ3SU7iRipiIhQtovw==} engines: {node: '>=14'} + deprecated: The gm module has been sunset. Please migrate to an alternative. https://github.com/aheckmann/gm?tab=readme-ov-file#2025-02-24-this-project-is-not-maintained gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} @@ -5851,6 +6004,7 @@ packages: graphql@14.0.0: resolution: {integrity: sha512-HGVcnO6B25YZcSt6ZsH6/N+XkYuPA7yMqJmlJ4JWxWlS4Tr8SHI56R1Ocs8Eor7V7joEZPRXPDH8RRdll1w44Q==} engines: {node: 6.x || 8.x || >= 10.x} + deprecated: 'No longer supported; please update to a newer version. Details: https://github.com/graphql/graphql-js#version-support' gzip-size@6.0.0: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} @@ -7269,10 +7423,6 @@ packages: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} - mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - mini-css-extract-plugin@2.9.2: resolution: {integrity: sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==} engines: {node: '>= 12.13.0'} @@ -7312,9 +7462,6 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -7412,9 +7559,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napi-build-utils@2.0.0: - resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} @@ -7451,16 +7595,9 @@ packages: node-2fa@2.0.3: resolution: {integrity: sha512-PQldrOhjuoZyoydMvMSctllPN1ZPZ1/NwkEcgYwY9faVqE/OymxR+3awPpbWZxm6acLKqvmNqQmdqTsqYyflFw==} - node-abi@3.73.0: - resolution: {integrity: sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==} - engines: {node: '>=10'} - node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} - node-addon-api@6.1.0: - resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} - node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} @@ -8399,11 +8536,6 @@ packages: resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} engines: {node: ^10 || ^12 || >=14} - prebuild-install@7.1.3: - resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} - engines: {node: '>=10'} - hasBin: true - prelude-ls@1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} engines: {node: '>= 0.8.0'} @@ -8455,6 +8587,10 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + promise-polyfill@6.1.0: resolution: {integrity: sha512-g0LWaH0gFsxovsU7R5LrrhHhWAWiHRnh1GPrhXnPgYsDkIqjRYUYSZEsej/wtleDrz5xVSIDbeKfidztp2XHFQ==} @@ -8562,10 +8698,6 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - react-app-env@1.2.3: resolution: {integrity: sha512-GmBiEvnHjJuSYBeEeSw/Kqp9r+tQDADnVe87z6yruJ5vohhH5x78WwnPnv7Lo/ygUDqddb7S0W+axOE1xvrbzQ==} hasBin: true @@ -8775,6 +8907,10 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + readdir-glob@1.1.3: resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} @@ -9141,6 +9277,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + send@0.19.0: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} @@ -9199,9 +9340,9 @@ packages: shallowequal@1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} - sharp@0.32.6: - resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} - engines: {node: '>=14.15.0'} + sharp@0.34.3: + resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} @@ -9252,12 +9393,6 @@ packages: signature_pad@2.3.2: resolution: {integrity: sha512-peYXLxOsIY6MES2TrRLDiNg2T++8gGbpP2yaC+6Ohtxr+a2dzoaqWosWDY9sWqTAAk6E/TyQO+LJw9zQwyu5kA==} - simple-concat@1.0.1: - resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - - simple-get@4.0.1: - resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} @@ -9349,6 +9484,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} @@ -9528,10 +9664,6 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -9543,6 +9675,10 @@ packages: strnum@1.0.5: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + strtok3@10.3.4: + resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} + engines: {node: '>=18'} + style-inject@0.3.0: resolution: {integrity: sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==} @@ -9643,12 +9779,6 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} - tar-fs@2.1.2: - resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==} - - tar-fs@3.0.8: - resolution: {integrity: sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==} - tar-stream@1.6.2: resolution: {integrity: sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==} engines: {node: '>= 0.8.0'} @@ -9781,6 +9911,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + token-types@6.1.1: + resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==} + engines: {node: '>=14.16'} + tough-cookie@4.1.4: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} @@ -9976,6 +10110,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + ulid@2.3.0: resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} hasBin: true @@ -10556,6 +10694,10 @@ packages: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -13161,6 +13303,8 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@borewit/text-codec@0.1.1': {} + '@changesets/apply-release-plan@7.0.8': dependencies: '@changesets/config': 3.0.5 @@ -13394,6 +13538,11 @@ snapshots: effect: 3.5.7 fast-check: 3.20.0 + '@emnapi/runtime@1.5.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.25.9 @@ -13522,6 +13671,13 @@ snapshots: lodash.isfunction: 3.0.9 lodash.isnil: 4.0.0 + '@fast-csv/format@5.0.5': + dependencies: + lodash.escaperegexp: 4.1.2 + lodash.isboolean: 3.0.3 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + '@fast-csv/parse@4.3.6': dependencies: '@types/node': 14.18.63 @@ -13544,6 +13700,92 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} + '@img/sharp-darwin-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.0 + optional: true + + '@img/sharp-darwin-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.0 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.0': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.0': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.0': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + optional: true + + '@img/sharp-linux-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.0 + optional: true + + '@img/sharp-linux-arm@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.0 + optional: true + + '@img/sharp-linux-ppc64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.0 + optional: true + + '@img/sharp-linux-s390x@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.0 + optional: true + + '@img/sharp-linux-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.0 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + optional: true + + '@img/sharp-wasm32@0.34.3': + dependencies: + '@emnapi/runtime': 1.5.0 + optional: true + + '@img/sharp-win32-arm64@0.34.3': + optional: true + + '@img/sharp-win32-ia32@0.34.3': + optional: true + + '@img/sharp-win32-x64@0.34.3': + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -14128,7 +14370,7 @@ snapshots: metro-config: 0.81.0 metro-core: 0.81.0 readline: 1.3.0 - semver: 7.6.3 + semver: 7.7.2 transitivePeerDependencies: - '@babel/core' - '@babel/preset-env' @@ -14802,6 +15044,16 @@ snapshots: '@tanstack/query-core': 5.65.0 react: 18.3.1 + '@tokenizer/inflate@0.2.7': + dependencies: + debug: 4.4.0 + fflate: 0.8.2 + token-types: 6.1.1 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + '@tootallnate/once@1.1.2': {} '@trysound/sax@0.2.0': {} @@ -14814,6 +15066,10 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@types/archiver@6.0.3': + dependencies: + '@types/readdir-glob': 1.1.5 + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.26.7 @@ -15055,6 +15311,10 @@ snapshots: '@types/prop-types': 15.7.14 csstype: 3.1.3 + '@types/readdir-glob@1.1.5': + dependencies: + '@types/node': 20.17.16 + '@types/resolve@1.17.1': dependencies: '@types/node': 20.17.16 @@ -15386,12 +15646,12 @@ snapshots: '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.97.1)': dependencies: webpack: 5.97.1(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-dev-server@4.15.2)(webpack@5.97.1) + webpack-cli: 5.1.4(webpack@5.97.1) '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.97.1)': dependencies: webpack: 5.97.1(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-dev-server@4.15.2)(webpack@5.97.1) + webpack-cli: 5.1.4(webpack@5.97.1) '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack-dev-server@4.15.2)(webpack@5.97.1)': dependencies: @@ -15560,6 +15820,16 @@ snapshots: normalize-path: 3.0.0 readable-stream: 3.6.2 + archiver-utils@5.0.2: + dependencies: + glob: 10.4.5 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.17.21 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + archiver@5.3.2: dependencies: archiver-utils: 2.1.0 @@ -15570,6 +15840,16 @@ snapshots: tar-stream: 2.2.0 zip-stream: 4.1.1 + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.7.0 + readdir-glob: 1.1.3 + tar-stream: 3.1.7 + zip-stream: 6.0.1 + arg@4.1.3: {} arg@5.0.2: {} @@ -15941,30 +16221,6 @@ snapshots: bare-events@2.5.4: optional: true - bare-fs@4.0.1: - dependencies: - bare-events: 2.5.4 - bare-path: 3.0.0 - bare-stream: 2.6.4(bare-events@2.5.4) - transitivePeerDependencies: - - bare-buffer - optional: true - - bare-os@3.4.0: - optional: true - - bare-path@3.0.0: - dependencies: - bare-os: 3.4.0 - optional: true - - bare-stream@2.6.4(bare-events@2.5.4): - dependencies: - streamx: 2.21.1 - optionalDependencies: - bare-events: 2.5.4 - optional: true - base64-js@1.5.1: {} batch@0.6.1: {} @@ -16124,6 +16380,8 @@ snapshots: buffer-crc32@0.2.13: {} + buffer-crc32@1.0.0: {} + buffer-equal-constant-time@1.0.1: {} buffer-fill@1.0.0: @@ -16144,6 +16402,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + buffers@0.1.1: {} builtin-modules@3.3.0: {} @@ -16273,8 +16536,6 @@ snapshots: dependencies: readdirp: 4.1.1 - chownr@1.1.4: {} - chrome-launcher@0.15.2: dependencies: '@types/node': 20.17.16 @@ -16417,6 +16678,14 @@ snapshots: normalize-path: 3.0.0 readable-stream: 3.6.2 + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + compressible@2.0.18: dependencies: mime-db: 1.53.0 @@ -16542,6 +16811,11 @@ snapshots: crc-32: 1.2.2 readable-stream: 3.6.2 + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + create-require@1.1.1: {} cross-env@3.2.4: @@ -16788,10 +17062,6 @@ snapshots: mimic-response: 1.0.1 optional: true - decompress-response@6.0.0: - dependencies: - mimic-response: 3.1.0 - decompress-tar@4.1.1: dependencies: file-type: 5.2.0 @@ -16837,8 +17107,6 @@ snapshots: dedent@0.7.0: {} - deep-extend@0.6.0: {} - deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -16882,7 +17150,7 @@ snapshots: detect-libc@1.0.3: optional: true - detect-libc@2.0.3: {} + detect-libc@2.0.4: {} detect-newline@3.1.0: {} @@ -17683,8 +17951,6 @@ snapshots: exit@0.1.2: {} - expand-template@2.0.3: {} - expect@27.5.1: dependencies: '@jest/types': 27.5.1 @@ -17829,6 +18095,8 @@ snapshots: dependencies: pend: 1.2.0 + fflate@0.8.2: {} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -17844,6 +18112,15 @@ snapshots: file-type@12.4.2: {} + file-type@21.0.0: + dependencies: + '@tokenizer/inflate': 0.2.7 + strtok3: 10.3.4 + token-types: 6.1.1 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + file-type@3.9.0: optional: true @@ -18159,8 +18436,6 @@ snapshots: git-hooks-list@1.0.3: {} - github-from-package@0.0.0: {} - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -19495,10 +19770,10 @@ snapshots: jmespath@0.16.0: {} - join-images@1.1.5(sharp@0.32.6): + join-images@1.1.5(sharp@0.34.3): dependencies: is-plain-obj: 3.0.0 - sharp: 0.32.6 + sharp: 0.34.3 tslib: 2.8.1 js-cookie@2.2.1: {} @@ -20162,8 +20437,6 @@ snapshots: mimic-response@1.0.1: optional: true - mimic-response@3.1.0: {} - mini-css-extract-plugin@2.9.2(webpack@5.97.1): dependencies: schema-utils: 4.3.0 @@ -20198,8 +20471,6 @@ snapshots: minipass@7.1.2: {} - mkdirp-classic@0.5.3: {} - mkdirp@0.5.6: dependencies: minimist: 1.2.8 @@ -20333,8 +20604,6 @@ snapshots: nanoid@3.3.8: {} - napi-build-utils@2.0.0: {} - natural-compare-lite@1.4.0: {} natural-compare@1.4.0: {} @@ -20383,14 +20652,8 @@ snapshots: thirty-two: 1.0.2 tslib: 2.8.1 - node-abi@3.73.0: - dependencies: - semver: 7.6.3 - node-abort-controller@3.1.1: {} - node-addon-api@6.1.0: {} - node-addon-api@7.1.1: optional: true @@ -21324,21 +21587,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prebuild-install@7.1.3: - dependencies: - detect-libc: 2.0.3 - expand-template: 2.0.3 - github-from-package: 0.0.0 - minimist: 1.2.8 - mkdirp-classic: 0.5.3 - napi-build-utils: 2.0.0 - node-abi: 3.73.0 - pump: 3.0.2 - rc: 1.2.8 - simple-get: 4.0.1 - tar-fs: 2.1.2 - tunnel-agent: 0.6.0 - prelude-ls@1.1.2: {} prelude-ls@1.2.1: {} @@ -21385,6 +21633,8 @@ snapshots: process-nextick-args@2.0.1: {} + process@0.11.10: {} + promise-polyfill@6.1.0: {} promise.series@0.2.0: {} @@ -21424,6 +21674,7 @@ snapshots: dependencies: end-of-stream: 1.4.4 once: 1.4.0 + optional: true punycode@1.3.2: {} @@ -21484,13 +21735,6 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - rc@1.2.8: - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - react-app-env@1.2.3: dependencies: cross-env: 3.2.4 @@ -21649,7 +21893,7 @@ snapshots: react-refresh: 0.14.2 regenerator-runtime: 0.13.11 scheduler: 0.24.0-canary-efb381bbf-20230505 - semver: 7.6.3 + semver: 7.7.2 stacktrace-parser: 0.1.10 whatwg-fetch: 3.6.20 ws: 6.2.3 @@ -21881,6 +22125,14 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + readdir-glob@1.1.3: dependencies: minimatch: 5.1.6 @@ -22266,6 +22518,8 @@ snapshots: semver@7.6.3: {} + semver@7.7.2: {} + send@0.19.0: dependencies: debug: 2.6.9 @@ -22355,18 +22609,34 @@ snapshots: shallowequal@1.1.0: {} - sharp@0.32.6: + sharp@0.34.3: dependencies: color: 4.2.3 - detect-libc: 2.0.3 - node-addon-api: 6.1.0 - prebuild-install: 7.1.3 - semver: 7.6.3 - simple-get: 4.0.1 - tar-fs: 3.0.8 - tunnel-agent: 0.6.0 - transitivePeerDependencies: - - bare-buffer + detect-libc: 2.0.4 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.3 + '@img/sharp-darwin-x64': 0.34.3 + '@img/sharp-libvips-darwin-arm64': 1.2.0 + '@img/sharp-libvips-darwin-x64': 1.2.0 + '@img/sharp-libvips-linux-arm': 1.2.0 + '@img/sharp-libvips-linux-arm64': 1.2.0 + '@img/sharp-libvips-linux-ppc64': 1.2.0 + '@img/sharp-libvips-linux-s390x': 1.2.0 + '@img/sharp-libvips-linux-x64': 1.2.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + '@img/sharp-linux-arm': 0.34.3 + '@img/sharp-linux-arm64': 0.34.3 + '@img/sharp-linux-ppc64': 0.34.3 + '@img/sharp-linux-s390x': 0.34.3 + '@img/sharp-linux-x64': 0.34.3 + '@img/sharp-linuxmusl-arm64': 0.34.3 + '@img/sharp-linuxmusl-x64': 0.34.3 + '@img/sharp-wasm32': 0.34.3 + '@img/sharp-win32-arm64': 0.34.3 + '@img/sharp-win32-ia32': 0.34.3 + '@img/sharp-win32-x64': 0.34.3 shebang-command@1.2.0: dependencies: @@ -22418,14 +22688,6 @@ snapshots: signature_pad@2.3.2: {} - simple-concat@1.0.1: {} - - simple-get@4.0.1: - dependencies: - decompress-response: 6.0.0 - once: 1.4.0 - simple-concat: 1.0.1 - simple-swizzle@0.2.2: dependencies: is-arrayish: 0.3.2 @@ -22756,8 +23018,6 @@ snapshots: strip-final-newline@2.0.0: {} - strip-json-comments@2.0.1: {} - strip-json-comments@3.1.1: {} strip-outer@1.0.1: @@ -22767,6 +23027,10 @@ snapshots: strnum@1.0.5: {} + strtok3@10.3.4: + dependencies: + '@tokenizer/token': 0.3.0 + style-inject@0.3.0: {} style-loader@3.3.4(webpack@5.97.1): @@ -22936,23 +23200,6 @@ snapshots: tapable@2.2.1: {} - tar-fs@2.1.2: - dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.2 - tar-stream: 2.2.0 - - tar-fs@3.0.8: - dependencies: - pump: 3.0.2 - tar-stream: 3.1.7 - optionalDependencies: - bare-fs: 4.0.1 - bare-path: 3.0.0 - transitivePeerDependencies: - - bare-buffer - tar-stream@1.6.2: dependencies: bl: 1.2.3 @@ -23086,6 +23333,12 @@ snapshots: toidentifier@1.0.1: {} + token-types@6.1.1: + dependencies: + '@borewit/text-codec': 0.1.1 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + tough-cookie@4.1.4: dependencies: psl: 1.15.0 @@ -23181,6 +23434,7 @@ snapshots: tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 + optional: true turbo-darwin-64@1.13.4: optional: true @@ -23304,6 +23558,8 @@ snapshots: typescript@5.7.3: {} + uint8array-extras@1.5.0: {} + ulid@2.3.0: {} unbox-primitive@1.1.0: @@ -23652,7 +23908,7 @@ snapshots: watchpack: 2.4.2 webpack-sources: 3.2.3 optionalDependencies: - webpack-cli: 5.1.4(webpack-dev-server@4.15.2)(webpack@5.97.1) + webpack-cli: 5.1.4(webpack@5.97.1) transitivePeerDependencies: - '@swc/core' - esbuild @@ -24023,3 +24279,9 @@ snapshots: archiver-utils: 3.0.4 compress-commons: 4.1.2 readable-stream: 3.6.2 + + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.7.0