UNPKG

@overture-stack/lyric

Version:
324 lines (323 loc) 18.4 kB
import { isEmpty } from 'lodash-es'; import { shouldBypassAuth } from '../middleware/auth.js'; import { getSubmittedFileType } from '../services/submission/submissionFile.js'; import createSubmissionService from '../services/submission/submissionService.js'; import createSubmittedDataService from '../services/submittedData/submmittedData.js'; import { hasUserWriteAccess } from '../utils/authUtils.js'; import { BadRequest, Forbidden, NotFound, StatusConflict } from '../utils/errors.js'; import { asArray } from '../utils/formatUtils.js'; import { validateRequest } from '../utils/requestValidation.js'; import { dataDeleteBySystemIdRequestSchema, editSingleEntityRequestSchema, submissionActiveByOrganizationRequestSchema, submissionByIdRequestSchema, submissionCommitRequestSchema, submissionDeleteEntityNameRequestSchema, submissionDeleteRequestSchema, submissionDetailsRequestSchema, submissionsByCategoryRequestSchema, uploadSingleEntitySubmissionDataRequestSchema, uploadSubmissionRequestSchema, } from '../utils/schemas.js'; import { isSubmissionActive, parseSubmissionActionTypes } from '../utils/submissionUtils.js'; import { BATCH_ERROR_TYPE, SUBMISSION_ACTION_TYPE, } from '../utils/types.js'; const controller = ({ baseDependencies, authConfig, }) => { const submissionService = createSubmissionService(baseDependencies); const dataService = createSubmittedDataService(baseDependencies); const { logger } = baseDependencies; const defaultPage = 1; const defaultPageSize = 20; const LOG_MODULE = 'SUBMISSION_CONTROLLER'; return { commit: validateRequest(submissionCommitRequestSchema, async (req, res, next) => { try { const categoryId = Number(req.params.categoryId); const submissionId = Number(req.params.submissionId); const user = req.user; logger.info(LOG_MODULE, `Request Commit Active Submission '${submissionId}' on category '${categoryId}'`); const submission = await submissionService.getSubmissionById(submissionId); if (!submission) { throw new BadRequest(`Submission '${submissionId}' not found`); } if (!shouldBypassAuth(req, authConfig) && !hasUserWriteAccess(submission.organization, user)) { throw new Forbidden(`User is not authorized to commit the submission from '${submission.organization}'`); } const username = user?.username || ''; const commitSubmission = await submissionService.commitSubmission(categoryId, submissionId, username); return res.status(200).send(commitSubmission); } catch (error) { next(error); } }), delete: validateRequest(submissionDeleteRequestSchema, async (req, res, next) => { try { const submissionId = Number(req.params.submissionId); const user = req.user; const force = req.query.force?.toLowerCase() === 'true'; logger.info(LOG_MODULE, `Request Delete Active Submission '${submissionId}'`); const submission = await submissionService.getSubmissionById(submissionId); if (!submission) { throw new BadRequest(`Submission '${submissionId}' not found`); } if (!shouldBypassAuth(req, authConfig) && !hasUserWriteAccess(submission.organization, user)) { throw new Forbidden(`User is not authorized to delete the submission from '${submission.organization}'`); } if (!isSubmissionActive(submission.status) && !force) { throw new StatusConflict('Only active Submissions can be deleted'); } const username = user?.username || ''; const deleteSubmissionResult = await submissionService.deleteActiveSubmissionById(submissionId, username, force); if (isEmpty(deleteSubmissionResult)) { throw new NotFound('Active Submission not found'); } return res.status(200).send(deleteSubmissionResult); } catch (error) { next(error); } }), deleteEntityName: validateRequest(submissionDeleteEntityNameRequestSchema, async (req, res, next) => { try { const submissionId = Number(req.params.submissionId); const actionType = SUBMISSION_ACTION_TYPE.parse(req.params.actionType.toUpperCase()); const entityName = req.query.entityName; const index = req.query.index ? parseInt(req.query.index) : null; const user = req.user; logger.info(LOG_MODULE, `Request Delete '${entityName ? entityName : 'all'}' records on '{${actionType}}' Active Submission '${submissionId}'`); const submission = await submissionService.getSubmissionById(submissionId); if (!submission) { throw new BadRequest(`Submission '${submissionId}' not found`); } if (!shouldBypassAuth(req, authConfig) && !hasUserWriteAccess(submission.organization, user)) { throw new Forbidden(`User is not authorized to delete the submission data from '${submission.organization}'`); } const username = user?.username || ''; const deleteSubmissionEntityResult = await submissionService.deleteActiveSubmissionEntity(submissionId, username, { actionType, entityName, index, }); if (isEmpty(deleteSubmissionEntityResult)) { throw new NotFound('Active Submission not found'); } return res.status(200).send(deleteSubmissionEntityResult); } catch (error) { next(error); } }), deleteSubmittedDataBySystemId: validateRequest(dataDeleteBySystemIdRequestSchema, async (req, res, next) => { try { const categoryId = Number(req.params.categoryId); const systemId = req.params.systemId; const user = req.user; logger.info(LOG_MODULE, `Request Delete Submitted Data systemId '${systemId}' on categoryId '${categoryId}'`); // get SubmittedData by SystemId const foundRecordToDelete = await dataService.getSubmittedDataBySystemId(categoryId, systemId, { view: 'flat', }); if (!foundRecordToDelete.result) { throw new BadRequest(`No Submitted data found with systemId '${systemId}'`); } if (!shouldBypassAuth(req, authConfig) && !hasUserWriteAccess(foundRecordToDelete.result.organization, user)) { throw new Forbidden(`User is not authorized to delete data from '${foundRecordToDelete.result?.organization}'`); } const username = user?.username || ''; const deletedRecordsResult = await dataService.deleteSubmittedDataBySystemId(categoryId, systemId, username); return res.status(200).send(deletedRecordsResult); } catch (error) { next(error); } }), editSubmittedData: validateRequest(editSingleEntityRequestSchema, async (req, res, next) => { try { const categoryId = Number(req.params.categoryId); const entityName = req.query.entityName; const organization = req.query.organization; const payload = req.body; const user = req.user; logger.info(LOG_MODULE, `Request Edit Submitted Data`); if (!payload || payload.length == 0) { throw new BadRequest('The "payload" parameter is missing or empty. Please include the records in the request for processing.'); } if (!shouldBypassAuth(req, authConfig) && !hasUserWriteAccess(organization, user)) { throw new Forbidden(`User is not authorized to edit data from '${organization}'`); } const username = user?.username || ''; const editSubmittedDataResult = await dataService.editSubmittedData({ records: payload, entityName, categoryId, organization, username, }); // This response provides the details of data Submission return res.status(200).send(editSubmittedDataResult); } catch (error) { next(error); } }), getSubmissionsByCategory: validateRequest(submissionsByCategoryRequestSchema, async (req, res, next) => { try { const categoryId = Number(req.params.categoryId); const onlyActive = req.query.onlyActive?.toLowerCase() === 'true'; const organization = req.query.organization; const page = parseInt(String(req.query.page)) || defaultPage; const pageSize = parseInt(String(req.query.pageSize)) || defaultPageSize; const username = req.query.username; logger.info(LOG_MODULE, `Request Submission categoryId '${categoryId}'`, `pagination params: page '${page}' pageSize '${pageSize}'`, `onlyActive '${onlyActive}'`, `organization '${organization}'`); const submissionsResult = await submissionService.getSubmissionsByCategory(categoryId, { page, pageSize }, { onlyActive, username, organization }); const response = { pagination: { currentPage: page, pageSize: pageSize, totalPages: Math.ceil(submissionsResult.metadata.totalRecords / pageSize), totalRecords: submissionsResult.metadata.totalRecords, }, records: submissionsResult.result, }; return res.status(200).send(response); } catch (error) { next(error); } }), getSubmissionById: validateRequest(submissionByIdRequestSchema, async (req, res, next) => { try { const submissionId = Number(req.params.submissionId); logger.info(LOG_MODULE, `Request Active Submission submissionId '${submissionId}'`); const submission = await submissionService.getSubmissionById(submissionId); if (isEmpty(submission)) { throw new NotFound('Submission not found'); } return res.status(200).send(submission); } catch (error) { next(error); } }), getSubmissionDetailsById: validateRequest(submissionDetailsRequestSchema, async (req, res, next) => { try { const submissionId = Number(req.params.submissionId); const entityNames = asArray(req.query.entityNames || []); const actionTypes = parseSubmissionActionTypes(req.query.actionTypes || SUBMISSION_ACTION_TYPE.options); // query params const page = parseInt(String(req.query.page)) || defaultPage; const pageSize = parseInt(String(req.query.pageSize)) || defaultPageSize; logger.info(LOG_MODULE, `Request Submission Details by ID '${submissionId}'`); const submission = await submissionService.getSubmissionDetailsById({ submissionId, paginationOptions: { page, pageSize }, filterOptions: { entityNames, actionTypes }, }); if (isEmpty(submission)) { throw new NotFound('Submission not found'); } return res.status(200).send(submission); } catch (error) { next(error); } }), getActiveByOrganization: validateRequest(submissionActiveByOrganizationRequestSchema, async (req, res, next) => { try { const categoryId = Number(req.params.categoryId); const organization = req.params.organization; logger.info(LOG_MODULE, `Request Active Submission categoryId '${categoryId}' and organization '${organization}'`); // Get username from auth const username = req.user?.username || ''; const activeSubmission = await submissionService.getActiveSubmissionByOrganization({ categoryId, username, organization, }); if (isEmpty(activeSubmission)) { throw new NotFound('Active Submission not found'); } return res.status(200).send(activeSubmission); } catch (error) { next(error); } }), submit: validateRequest(uploadSingleEntitySubmissionDataRequestSchema, async (req, res, next) => { try { const categoryId = Number(req.params.categoryId); const entityName = req.query.entityName; const organization = req.query.organization; const payload = req.body; const user = req.user; // TODO: Validate file-entity map in body: no duplicate filenames, and that entities exist in schemaNames logger.info(LOG_MODULE, `Submission Request: categoryId '${categoryId}'`, ` organization '${organization}'`, ` entityName '${entityName}'`); // TODO: parse body payload if (!payload || !Array.isArray(payload) || payload.length == 0) { throw new BadRequest('The "payload" parameter is missing or empty. Please include the records in the request for processing.'); } if (!shouldBypassAuth(req, authConfig) && !hasUserWriteAccess(organization, user)) { throw new Forbidden(`User is not authorized to submit data to '${organization}'`); } const username = user?.username || ''; const resultSubmission = await submissionService.submit({ data: { [entityName]: payload }, categoryId, organization, username, }); if (resultSubmission.status === 'UNKNOWN_CATEGORY') { throw new BadRequest(resultSubmission.description); } // This response provides the details of data Submission return res.status(200).send(resultSubmission); } catch (error) { next(error); } }), submitFiles: validateRequest(uploadSubmissionRequestSchema, async (req, res, next) => { try { const categoryId = Number(req.params.categoryId); const files = Array.isArray(req.files) ? req.files : []; const organization = req.query.organization; const fileEntityMap = req.body; // Get username from auth const username = req.user?.username || ''; logger.info(LOG_MODULE, `Upload Submission Request: categoryId '${categoryId}'`, ` organization '${organization}'`, ` files '${files?.map((f) => f.originalname)}'`, ` fileEntityMap ${JSON.stringify(fileEntityMap)}`); if (!shouldBypassAuth(req, authConfig) && !hasUserWriteAccess(organization, req.user)) { throw new Forbidden(`User is not authorized to submit data to '${organization}'`); } if (!files || files.length == 0) { throw new BadRequest('The "files" parameter is missing or empty. Please include files in the request for processing.'); } // sort files into validFiles and fileErrors based on correct file extension const { validFiles, fileErrors } = files.reduce((acc, file) => { const fileTypeResult = getSubmittedFileType(file); if (fileTypeResult.success) { acc.validFiles.push(file); } else { const batchError = { type: BATCH_ERROR_TYPE.INVALID_FILE_EXTENSION, message: fileTypeResult.data.message, batchName: file.originalname, }; acc.fileErrors.push(batchError); } return acc; }, { validFiles: [], fileErrors: [] }); const submitFilesResult = await submissionService.submitFiles({ files: validFiles, categoryId, organization, username, fileEntityMap, }); if (submitFilesResult.status === 'UNKNOWN_CATEGORY') { throw new BadRequest(submitFilesResult.description); } // If any files were successfully accepted and processing has started, we return 200 (maybe with batch errors) // otherwise, return 400 since nothing the user sent was successful. const responseStatus = submitFilesResult.inProcessEntities.length > 0 ? 200 : 400; return res .status(responseStatus) .send({ ...submitFilesResult, batchErrors: [...fileErrors, ...submitFilesResult.batchErrors] }); // This response provides the details of file Submission } catch (error) { next(error); } }), }; }; export default controller;