@overture-stack/lyric
Version:
Data Submission system
324 lines (323 loc) • 18.4 kB
JavaScript
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;