Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/10 crud analytics #18

Merged
merged 3 commits into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
151 changes: 151 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
99 changes: 57 additions & 42 deletions src/controllers/fileController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -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) {
Expand All @@ -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' });
Expand All @@ -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' });
}

Expand All @@ -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
};
2 changes: 1 addition & 1 deletion src/models/ClinicalHistory.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const clinicalHistorySchema = new Schema({
required: true,
default: [],
},
analitycs: {
analytics: {
type: [fileSchema],
required: true,
default: [],
Expand Down
22 changes: 1 addition & 21 deletions src/models/Treatment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading