From d9e2619a31c208e1238c3cd25b26f6b2eb6318f5 Mon Sep 17 00:00:00 2001 From: Daniel Arriaza Arriaza <113770002+darkgigi@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:24:40 +0100 Subject: [PATCH 1/3] fix: changed schema validations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Rafael González Castillero <64226756+rafgoncas1@users.noreply.github.com> --- src/models/ClinicalHistory.js | 2 +- src/models/Treatment.js | 22 +------- .../controllers/treatmentController.test.js | 4 +- .../schemas/clinicalHistorySchema.test.js | 56 +++---------------- 4 files changed, 12 insertions(+), 72 deletions(-) diff --git a/src/models/ClinicalHistory.js b/src/models/ClinicalHistory.js index 73d2779..528b79c 100644 --- a/src/models/ClinicalHistory.js +++ b/src/models/ClinicalHistory.js @@ -37,7 +37,7 @@ const clinicalHistorySchema = new Schema({ required: true, default: [], }, - analitycs: { + analytics: { type: [fileSchema], required: true, default: [], diff --git a/src/models/Treatment.js b/src/models/Treatment.js index 71fb8da..759edb4 100644 --- a/src/models/Treatment.js +++ b/src/models/Treatment.js @@ -12,38 +12,18 @@ const treatmentSchema = new mongoose.Schema({ const today = new Date(); const utc = new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate())); return utc; - }, - validate: { - validator: function(value) { - // Verify that startDate is not in the past - const today = new Date(); - const utc = new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate())); - return value >= utc; - }, - message: 'Start date must be today or in the future' } }, endDate: { type: Date, required: [true, 'End date is required'], - validate: [ - { - validator: function(value) { - // Verify that endDate is not in the past - const today = new Date(); - const utc = new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate())); - return value >= utc; - }, - message: 'End date must be today or in the future' - }, - { + validate: { validator: function(value) { // Verify that endDate is greater than or equal to startDate return !this.startDate || value >= this.startDate; }, message: 'End date must be greater than or equal to start date' } - ] }, instructions: { type: String, diff --git a/tests/unit/controllers/treatmentController.test.js b/tests/unit/controllers/treatmentController.test.js index 5f01541..ca3d835 100644 --- a/tests/unit/controllers/treatmentController.test.js +++ b/tests/unit/controllers/treatmentController.test.js @@ -41,7 +41,7 @@ describe('CLINICAL HISTORY TREATMENT ENDPOINTS TEST', () => { const response = await request.post(`/histories/${newClinicalHistory._id}/treatment`).send(treatmentData); expect(response.status).toBe(400); - expect(response.body.errors['treatments.0.endDate']).toContain('End date must be today or in the future'); + expect(response.body.errors['treatments.0.endDate']).toContain('End date must be greater than or equal to start date'); }); it('should return 200 and add treatment if clinical history exists', async () => { @@ -146,7 +146,7 @@ describe('CLINICAL HISTORY TREATMENT ENDPOINTS TEST', () => { const response = await request.put(`/histories/${newClinicalHistory._id}/treatment/${treatmentId}`).send(updatedTreatmentData); expect(response.status).toBe(400); - expect(response.body.errors['treatments.0.endDate']).toContain('End date must be today or in the future'); + expect(response.body.errors['treatments.0.endDate']).toContain('End date must be greater than or equal to start date'); }); it('should return 200 and update treatment if treatment exists', async () => { diff --git a/tests/unit/schemas/clinicalHistorySchema.test.js b/tests/unit/schemas/clinicalHistorySchema.test.js index 3a51413..85029cc 100644 --- a/tests/unit/schemas/clinicalHistorySchema.test.js +++ b/tests/unit/schemas/clinicalHistorySchema.test.js @@ -17,7 +17,7 @@ describe('CLINICAL HISTORY SCHEMA TESTS', () => { images: [ { name: 'X-ray', url: 'http://example.com/xray.jpg', date: new Date(), originalName: 'xray.jpg' } ], - analitycs: [ + analytics: [ { name: 'Blood Test', url: 'http://example.com/bloodtest.pdf', date: new Date(), originalName: 'bloodtest.pdf' } ], allergies: ['Peanuts', 'Dust'] @@ -34,7 +34,7 @@ describe('CLINICAL HISTORY SCHEMA TESTS', () => { currentConditions: [], treatments: [], images: [], - analitycs: [], + analytics: [], allergies: [] }; @@ -51,7 +51,7 @@ describe('CLINICAL HISTORY SCHEMA TESTS', () => { ], treatments: [], images: [], - analitycs: [], + analytics: [], allergies: [] }; @@ -68,7 +68,7 @@ describe('CLINICAL HISTORY SCHEMA TESTS', () => { images: [ { name: 'X-ray', url: 'invalid-url', date: new Date(), originalName: 'xray.jpg' } ], - analitycs: [], + analytics: [], allergies: [] }; @@ -85,7 +85,7 @@ describe('CLINICAL HISTORY SCHEMA TESTS', () => { { startDate: new Date(), endDate: new Date(), instructions: 'Take once daily' } // Missing 'name' ], images: [], - analitycs: [], + analytics: [], allergies: [] }; @@ -106,52 +106,12 @@ describe('CLINICAL HISTORY SCHEMA TESTS', () => { ], currentConditions: [], images: [], - analitycs: [], + analytics: [], allergies: [] }; const clinicalHistory = new ClinicalHistory(clinicalHistoryData); - await expect(clinicalHistory.validate()).rejects.toThrow('End date must be today or in the future'); - }); - - it('should throw validation error if startDate is before today', async () => { - const now = new Date(); - const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 1 day before - - const clinicalHistoryData = { - _id: uuidv4(), - patientId: uuidv4(), - treatments: [ - { name: 'Invalid Treatment', startDate: pastDate, endDate: now, instructions: 'Take once daily' } - ], - currentConditions: [], - images: [], - analitycs: [], - allergies: [] - }; - - const clinicalHistory = new ClinicalHistory(clinicalHistoryData); - await expect(clinicalHistory.validate()).rejects.toThrow('Start date must be today or in the future'); - }); - - it('should throw validation error if endDate is before today', async () => { - const now = new Date(); - const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 1 day before - - const clinicalHistoryData = { - _id: uuidv4(), - patientId: uuidv4(), - treatments: [ - { name: 'Invalid Treatment', startDate: now, endDate: pastDate, instructions: 'Take once daily' } - ], - currentConditions: [], - images: [], - analitycs: [], - allergies: [] - }; - - const clinicalHistory = new ClinicalHistory(clinicalHistoryData); - await expect(clinicalHistory.validate()).rejects.toThrow('End date must be today or in the future'); + await expect(clinicalHistory.validate()).rejects.toThrow('End date must be greater than or equal to start date'); }); it('should throw validation error for currentCondition with date in the future', async () => { @@ -165,7 +125,7 @@ describe('CLINICAL HISTORY SCHEMA TESTS', () => { ], treatments: [], images: [], - analitycs: [], + analytics: [], allergies: [] }; From 3539d132a64d3954ec55358c5675e7578da2cc98 Mon Sep 17 00:00:00 2001 From: Daniel Arriaza Arriaza <113770002+darkgigi@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:25:24 +0100 Subject: [PATCH 2/3] feat: created analytic operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Rafael González Castillero <64226756+rafgoncas1@users.noreply.github.com> --- .env.example | 1 + src/controllers/fileController.js | 99 +++++++++++++++++------------ src/routes/clinicalHistoryRoutes.js | 6 +- 3 files changed, 62 insertions(+), 44 deletions(-) diff --git a/.env.example b/.env.example index 509935f..58d0111 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,4 @@ API_PREFIX=/api/v1 AZURE_SAS_TOKEN=sas_token AZURE_ACCOUNT_NAME=account_name AZURE_IMAGES_CONTAINER_NAME=container_name +AZURE_ANALYTICS_CONTAINER_NAME=container_name diff --git a/src/controllers/fileController.js b/src/controllers/fileController.js index 6364a79..e3fc6ae 100644 --- a/src/controllers/fileController.js +++ b/src/controllers/fileController.js @@ -6,14 +6,18 @@ import mongoose from 'mongoose'; const sasToken = process.env.AZURE_SAS_TOKEN; const accountName = process.env.AZURE_ACCOUNT_NAME; -const imagesContainerName = process.env.AZURE_IMAGES_CONTAINER_NAME; - +const containerNames = { + image: process.env.AZURE_IMAGES_CONTAINER_NAME, + analytic: process.env.AZURE_ANALYTICS_CONTAINER_NAME +}; const blobServiceClient = new BlobServiceClient(`https://${accountName}.blob.core.windows.net/?${sasToken}`); -const imagesContainerClient = blobServiceClient.getContainerClient(imagesContainerName); - -const uploadImageStream = async (fileName, filePath, mimeType) => { - const blobClient = imagesContainerClient.getBlockBlobClient(fileName); +const containerClients = { + image: blobServiceClient.getContainerClient(containerNames.image), + analytic: blobServiceClient.getContainerClient(containerNames.analytic) +}; +const uploadFileStream = async (fileName, filePath, mimeType, resourceType) => { + const blobClient = containerClients[resourceType].getBlockBlobClient(fileName); // Upload file from disk const fileBuffer = await fs.readFile(filePath); await blobClient.uploadData(fileBuffer, { @@ -26,8 +30,8 @@ const uploadImageStream = async (fileName, filePath, mimeType) => { return blobClient.url; } -const deleteBlob = async (blobName) => { - const blobClient = imagesContainerClient.getBlockBlobClient(blobName); +const deleteBlob = async (blobName, resourceType) => { + const blobClient = containerClients[resourceType].getBlockBlobClient(blobName); try { await blobClient.delete(); } catch (error) { @@ -41,8 +45,9 @@ const deleteBlob = async (blobName) => { } -const handleImageUpload = async (req, res) => { +const handleFileUpload = async (req, res) => { const { id } = req.params; + const resourceType = req.path.includes('/image') ? 'image' : 'analytic'; if (!id) { return res.status(400).json({ message: 'clinicalHistoryId is required' }); @@ -51,7 +56,7 @@ const handleImageUpload = async (req, res) => { const clinicalHistory = await ClinicalHistory.findById(id); if (!clinicalHistory) { - logger.error(`handleImageUpload - Clinical history with id ${id} was not found`); + logger.error(`handleFileUpload - Clinical history with id ${id} was not found`); return res.status(404).json({ message: 'Clinical history not found' }); } @@ -63,70 +68,80 @@ const handleImageUpload = async (req, res) => { try { - logger.debug(`handleImageUpload - Extracted metadata: ${fileName}, ${filePath}, ${mimeType}, ${originalName}`); + logger.debug(`handleFileUpload - Extracted metadata: ${fileName}, ${filePath}, ${mimeType}, ${originalName}`); - const imageUrl = await uploadImageStream(fileName, filePath, mimeType); - logger.info(`handleImageUpload - File uploaded to Azure Blob Storage: ${imageUrl}`); - - clinicalHistory.images.push({ name: fileName, url: imageUrl, originalName: originalName }); + const url = await uploadFileStream(fileName, filePath, mimeType, resourceType); + logger.info(`handleFileUpload - File uploaded to Azure Blob Storage: ${url}`); + + if (resourceType === 'image') { + clinicalHistory.images.push({ name: fileName, url: url, originalName: originalName }); + } else { + clinicalHistory.analytics.push({ name: fileName, url: url, originalName: originalName }); + } await clinicalHistory.save(); - logger.info(`handleImageUpload - Image saved to clinical history: ${fileName}`); + logger.info(`handleFileUpload - File saved to clinical history: ${fileName}`); - return res.status(201).json({ message: 'Image uploaded successfully', imageUrl }); + return res.status(201).json({ message: 'File uploaded successfully', url }); } catch (error) { - logger.error('handleImageUpload - An error ocurred while uploading the file'+error); + logger.error('handleFileUpload - An error ocurred while uploading the file'+error); - logger.info('handleImageUpload - Deleting blob and local file if exists'); + logger.info('handleFileUpload - Deleting blob and local file if exists'); await deleteBlob(fileName); await fs.unlink(filePath); - return res.status(500).json({ message: 'Error uploading image' }); + return res.status(500).json({ message: 'Error uploading file' }); } } -const deleteImage = async (req, res) => { - const { id, imageId } = req.params; +const deleteFile = async (req, res) => { + const { id, fileId } = req.params; + const resourceType = req.path.includes('/image') ? 'image' : 'analytic'; - if (!id || !imageId) { - return res.status(400).json({ message: 'clinicalHistoryId and imageId are required' }); + if (!id || !fileId) { + return res.status(400).json({ message: 'clinicalHistoryId and fileId are required' }); } const clinicalHistory = await ClinicalHistory.findById(id); if (!clinicalHistory) { - logger.error(`deleteImage - Clinical history with id ${id} was not found`); + logger.error(`deleteFile - Clinical history with id ${id} was not found`); return res.status(404).json({ message: 'Clinical history not found' }); } - if (!mongoose.Types.ObjectId.isValid(imageId)) { - logger.error(`deleteImage - Image ID ${imageId} is not a valid ObjectId`); - return res.status(400).json({ message: 'Image ID is not valid' }); + if (!mongoose.Types.ObjectId.isValid(fileId)) { + logger.error(`deleteFile - File ID ${fileId} is not a valid ObjectId`); + return res.status(400).json({ message: 'File ID is not valid' }); } - const image = clinicalHistory.images.id(imageId); + var file; + if (resourceType === 'image') { + file = clinicalHistory.images.id(fileId); + } else { + file = clinicalHistory.analytics.id(fileId); + } - if (!image) { - logger.error(`deleteImage - Image with id ${imageId} was not found`); - return res.status(404).json({ message: 'Image not found' }); + if (!file) { + logger.error(`deleteFile - File with id ${fileId} was not found`); + return res.status(404).json({ message: 'File not found' }); } try { - logger.debug(`deleteImage - Deleting image: ${image.name}`); - await deleteBlob(image.name); - logger.debug(`deleteImage - Image deleted from Azure Blob Storage: ${image.name}`); - image.deleteOne(); + logger.debug(`deleteFile - Deleting file: ${file.name}`); + await deleteBlob(file.name, resourceType); + logger.debug(`deleteFile - File deleted from Azure Blob Storage: ${file.name}`); + file.deleteOne(); await clinicalHistory.save(); - logger.info(`deleteImage - Image deleted successfully: ${image.name}`); - return res.status(200).json({ message: 'Image deleted successfully' }); + logger.info(`deleteFile - File deleted successfully: ${file.name}`); + return res.status(200).json({ message: 'File deleted successfully' }); } catch (error) { - logger.error('deleteImage - Error deleting the image', error); - return res.status(500).json({ message: 'Error deleting image' }); + logger.error('deleteFile - Error deleting the file', error); + return res.status(500).json({ message: 'Error deleting file' }); } } export { - handleImageUpload, - deleteImage + handleFileUpload, + deleteFile }; \ No newline at end of file diff --git a/src/routes/clinicalHistoryRoutes.js b/src/routes/clinicalHistoryRoutes.js index 269e46b..b780e78 100644 --- a/src/routes/clinicalHistoryRoutes.js +++ b/src/routes/clinicalHistoryRoutes.js @@ -25,7 +25,9 @@ router.delete('/:id/condition/:currentConditionId', currentConditionController.d router.put('/:id/condition/:currentConditionId', currentConditionController.updateCurrentCondition); // File routes -router.post('/:id/image', uploadMiddleware, fileController.handleImageUpload); -router.delete('/:id/image/:imageId', fileController.deleteImage); +router.post('/:id/image', uploadMiddleware, fileController.handleFileUpload); +router.delete('/:id/image/:fileId', fileController.deleteFile); +router.post('/:id/analytic', uploadMiddleware, fileController.handleFileUpload); +router.delete('/:id/analytic/:fileId', fileController.deleteFile); export default router; \ No newline at end of file From 58e8a6853d7696e3e82a1e32a60428e94da5667d Mon Sep 17 00:00:00 2001 From: Daniel Arriaza Arriaza <113770002+darkgigi@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:25:38 +0100 Subject: [PATCH 3/3] docs: updated openapi.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Rafael González Castillero <64226756+rafgoncas1@users.noreply.github.com> --- openapi.yaml | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/openapi.yaml b/openapi.yaml index 832a25e..2ebba28 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -407,6 +407,133 @@ paths: error: type: string example: Detailed error message. + /histories/{id}/analytic: + post: + summary: Upload an analytic to a clinical history record + description: Allows uploading an analytic to an existing clinical history record. + parameters: + - name: id + in: path + required: true + description: The ID of the clinical history record. + schema: + type: string + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + description: The analytic file to upload. + responses: + '201': + description: Analytic uploaded successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Analytic uploaded successfully + analyticUrl: + type: string + description: URL of the uploaded analytic. + '400': + description: Bad Request - Missing required headers or Clinical History ID. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: clinicalHistoryId is required + '404': + description: Clinical History record not found. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Clinical history not found + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Internal server error occurred. + /histories/{id}/analytic/{analyticId}: + delete: + summary: Delete an analytic from a clinical history record + description: Deletes a specific analytic associated with a clinical history record by its ID. Also removes the file from Azure Blob Storage. + parameters: + - name: id + in: path + required: true + description: The ID of the clinical history record. + schema: + type: string + - name: analyticId + in: path + required: true + description: The ID of the analytic to delete. + schema: + type: string + responses: + '200': + description: Analytic deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Analytic deleted successfully + '400': + description: Bad Request - Missing or invalid parameters. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: clinicalHistoryId and analyticId are required + '404': + description: Not Found - Clinical history or analytic not found. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Clinical history or analytic not found + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Error deleting analytic + error: + type: string + example: Detailed error message. components: schemas: ClinicalHistory: @@ -424,6 +551,14 @@ components: type: array items: $ref: '#/components/schemas/CurrentCondition' + images: + type: array + items: + $ref: '#/components/schemas/File' + analytics: + type: array + items: + $ref: '#/components/schemas/File' createdAt: type: string format: date-time @@ -474,3 +609,19 @@ components: required: - name - details + File: + type: object + properties: + name: + type: string + description: Name of the file + originalName: + type: string + description: Original name of the file + url: + type: string + description: URL of the file + date: + type: string + format: date-time + description: Date the file was uploaded