@overture-stack/lyric
Version:
Data Submission system
314 lines (313 loc) • 15.2 kB
JavaScript
import * as _ from 'lodash-es';
import systemIdGenerator from '../../external/systemIdGenerator.js';
import submissionRepository from '../../repository/activeSubmissionRepository.js';
import categoryRepository from '../../repository/categoryRepository.js';
import submittedRepository from '../../repository/submittedRepository.js';
import { BadRequest, InternalServerError, StatusConflict } from '../../utils/errors.js';
import { canTransitionToClosed, parseSubmissionResponse, parseSubmissionSummaryResponse, removeItemsFromSubmission, } from '../../utils/submissionUtils.js';
import { CREATE_SUBMISSION_STATUS, SUBMISSION_ACTION_TYPE, SUBMISSION_STATUS, } from '../../utils/types.js';
import processor from './processor.js';
const service = (dependencies) => {
const LOG_MODULE = 'SUBMISSION_SERVICE';
const { logger, onFinishCommit } = dependencies;
const { performCommitSubmissionAsync, performDataValidation } = processor(dependencies);
/**
* Runs Schema validation asynchronously and moves the Active Submission to Submitted Data
* @param {number} categoryId
* @param {number} submissionId
* @returns {Promise<CommitSubmissionResult>}
*/
const commitSubmission = async (categoryId, submissionId, username) => {
const { getSubmissionById } = submissionRepository(dependencies);
const { getSubmittedDataByCategoryIdAndOrganization } = submittedRepository(dependencies);
const { getActiveDictionaryByCategory } = categoryRepository(dependencies);
const { generateIdentifier } = systemIdGenerator(dependencies);
const submission = await getSubmissionById(submissionId);
if (!submission) {
throw new BadRequest(`Submission '${submissionId}' not found`);
}
if (submission.dictionaryCategoryId !== categoryId) {
throw new BadRequest(`Category ID provided does not match the category for the Submission`);
}
if (submission.status !== SUBMISSION_STATUS.VALID) {
throw new StatusConflict('Submission does not have status VALID and cannot be committed');
}
const currentDictionary = await getActiveDictionaryByCategory(categoryId);
if (_.isEmpty(currentDictionary)) {
throw new BadRequest(`Dictionary in category '${categoryId}' not found`);
}
const submittedDataToValidate = await getSubmittedDataByCategoryIdAndOrganization(categoryId, submission?.organization);
const entitiesToProcess = new Set();
submittedDataToValidate?.forEach((data) => entitiesToProcess.add(data.entityName));
const insertsToValidate = submission.data?.inserts
? Object.entries(submission.data.inserts).flatMap(([entityName, submissionData]) => {
entitiesToProcess.add(entityName);
return submissionData.records.map((record) => ({
data: record,
dictionaryCategoryId: categoryId,
entityName,
isValid: false, // By default, New Submitted Data is created as invalid until validation proves otherwise
organization: submission.organization,
originalSchemaId: submission.dictionaryId,
systemId: generateIdentifier(entityName, record),
createdBy: username,
}));
})
: [];
const deleteDataArray = submission.data?.deletes
? Object.entries(submission.data.deletes).flatMap(([entityName, submissionDeleteData]) => {
entitiesToProcess.add(entityName);
return submissionDeleteData;
})
: [];
const updateDataArray = submission.data?.updates &&
Object.entries(submission.data.updates).reduce((acc, [entityName, submissionUpdateData]) => {
entitiesToProcess.add(entityName);
submissionUpdateData.forEach((record) => {
acc[record.systemId] = record;
});
return acc;
}, {});
// To Commit Active Submission we need to validate SubmittedData + Active Submission
performCommitSubmissionAsync({
dataToValidate: {
inserts: insertsToValidate,
submittedData: submittedDataToValidate,
deletes: deleteDataArray,
updates: updateDataArray,
},
submission,
dictionary: currentDictionary,
username: username,
onFinishCommit,
});
return {
status: CREATE_SUBMISSION_STATUS.PROCESSING,
dictionary: {
name: currentDictionary.name,
version: currentDictionary.version,
},
processedEntities: Array.from(entitiesToProcess.values()),
};
};
/**
* Updates Submission status to CLOSED
* This action is allowed only if current Submission Status as OPEN, VALID or INVALID
* Returns the resulting Active Submission with its status
* @param {number} submissionId
* @param {string} username
* @returns {Promise<Submission | undefined>}
*/
const deleteActiveSubmissionById = async (submissionId, username) => {
const { getSubmissionById, update } = submissionRepository(dependencies);
const submission = await getSubmissionById(submissionId);
if (!submission) {
throw new BadRequest(`Submission '${submissionId}' not found`);
}
if (!canTransitionToClosed(submission.status)) {
throw new StatusConflict('Only Submissions with statuses "OPEN", "VALID", "INVALID" can be deleted');
}
const updatedRecord = await update(submission.id, {
status: SUBMISSION_STATUS.CLOSED,
updatedBy: username,
});
logger.info(LOG_MODULE, `Submission '${submissionId}' updated with new status '${SUBMISSION_STATUS.CLOSED}'`);
return updatedRecord;
};
/**
* Function to remove an entity from an Active Submission by given Submission ID
* It validates resulting Active Submission running cross schema validation along with the existing Submitted Data
* Returns the resulting Active Submission with its status
* @param {number} submissionId
* @param {string} entityName
* @param {string} username
* @returns { Promise<Submission | undefined>} Resulting Active Submittion
*/
const deleteActiveSubmissionEntity = async (submissionId, username, filter) => {
const { getSubmissionById } = submissionRepository(dependencies);
const submission = await getSubmissionById(submissionId);
if (!submission) {
throw new BadRequest(`Submission '${submissionId}' not found`);
}
if (SUBMISSION_ACTION_TYPE.Values.INSERTS.includes(filter.actionType) &&
!_.has(submission.data.inserts, filter.entityName)) {
throw new BadRequest(`Entity '${filter.entityName}' not found on '${filter.actionType}' Submission`);
}
if (SUBMISSION_ACTION_TYPE.Values.UPDATES.includes(filter.actionType) &&
!_.has(submission.data.updates, filter.entityName)) {
throw new BadRequest(`Entity '${filter.entityName}' not found on '${filter.actionType}' Submission`);
}
if (SUBMISSION_ACTION_TYPE.Values.DELETES.includes(filter.actionType) &&
!_.has(submission.data.deletes, filter.entityName)) {
throw new BadRequest(`Entity '${filter.entityName}' not found on '${filter.actionType}' Submission`);
}
// Remove entity from the Submission
const updatedActiveSubmissionData = removeItemsFromSubmission(submission.data, {
...filter,
});
const updatedRecord = await performDataValidation({
originalSubmission: submission,
submissionData: updatedActiveSubmissionData,
username,
});
logger.info(LOG_MODULE, `Submission '${updatedRecord.id}' updated with new status '${updatedRecord.status}'`);
return updatedRecord;
};
/**
* Get Submissions by Category
* @param {number} categoryId - The ID of the category for which data is being fetched.
* @param {Object} paginationOptions - Pagination properties
* @param {number} paginationOptions.page - Page number
* @param {number} paginationOptions.pageSize - Items per page
* @param {Object} filterOptions
* @param {boolean} filterOptions.onlyActive - Filter by Active status
* @param {string} filterOptions.username - User Name
* @returns an array of Submission
*/
const getSubmissionsByCategory = async (categoryId, paginationOptions, filterOptions) => {
const { getSubmissionsWithRelationsByCategory, getTotalSubmissionsByCategory } = submissionRepository(dependencies);
const recordsPaginated = await getSubmissionsWithRelationsByCategory(categoryId, paginationOptions, filterOptions);
if (!recordsPaginated || recordsPaginated.length === 0) {
return {
result: [],
metadata: {
totalRecords: 0,
},
};
}
const totalRecords = await getTotalSubmissionsByCategory(categoryId, filterOptions);
return {
metadata: {
totalRecords,
},
result: recordsPaginated.map((response) => parseSubmissionSummaryResponse(response)),
};
};
/**
* Get Submission by Submission ID
* @param {number} submissionId A Submission ID
* @returns One Submission
*/
const getSubmissionById = async (submissionId) => {
const { getSubmissionWithRelationsById } = submissionRepository(dependencies);
const submission = await getSubmissionWithRelationsById(submissionId);
if (_.isEmpty(submission)) {
return;
}
return parseSubmissionResponse(submission);
};
/**
* Get an active Submission by Organization
* @param {Object} params
* @param {number} params.categoryId
* @param {string} params.username
* @param {string} params.organization
* @returns One Active Submission
*/
const getActiveSubmissionByOrganization = async ({ categoryId, username, organization, }) => {
const { getActiveSubmissionWithRelationsByOrganization } = submissionRepository(dependencies);
const submission = await getActiveSubmissionWithRelationsByOrganization({ organization, username, categoryId });
if (_.isEmpty(submission)) {
return;
}
return parseSubmissionSummaryResponse(submission);
};
/**
* Find the current Active Submission or Create an Open Active Submission with initial values and no schema data.
* @param {object} params
* @param {string} params.username Owner of the Submission
* @param {number} params.categoryId Category ID of the Submission
* @param {string} params.organization Organization name
* @returns {Submission} An Active Submission
*/
const getOrCreateActiveSubmission = async (params) => {
const { categoryId, username, organization } = params;
const submissionRepo = submissionRepository(dependencies);
const categoryRepo = categoryRepository(dependencies);
const activeSubmission = await submissionRepo.getActiveSubmission({ categoryId, username, organization });
if (activeSubmission) {
return activeSubmission;
}
const currentDictionary = await categoryRepo.getActiveDictionaryByCategory(categoryId);
if (!currentDictionary) {
throw new InternalServerError(`Dictionary in category '${categoryId}' not found`);
}
const newSubmissionInput = {
createdBy: username,
data: {},
dictionaryCategoryId: categoryId,
dictionaryId: currentDictionary.id,
errors: {},
organization: organization,
status: SUBMISSION_STATUS.OPEN,
};
return submissionRepo.save(newSubmissionInput);
};
/**
* Validates and Creates the Entities Schemas of the Active Submission and stores it in the database
* @param {object} params
* @param {Record<string, unknown>[]} params.records An array of records
* @param {string} params.entityName Entity Name of the Records
* @param {number} params.categoryId Category ID of the Submission
* @param {string} params.organization Organization name
* @param {string} params.username User name creating the Submission
* @returns The Active Submission created or Updated
*/
const submit = async ({ records, entityName, categoryId, organization, username, }) => {
logger.info(LOG_MODULE, `Processing '${records.length}' records on category id '${categoryId}' organization '${organization}'`);
const { getActiveDictionaryByCategory } = categoryRepository(dependencies);
const { validateRecordsAsync } = processor(dependencies);
if (records.length === 0) {
return {
status: CREATE_SUBMISSION_STATUS.INVALID_SUBMISSION,
description: 'No valid records for submission',
};
}
const currentDictionary = await getActiveDictionaryByCategory(categoryId);
if (_.isEmpty(currentDictionary)) {
return {
status: CREATE_SUBMISSION_STATUS.INVALID_SUBMISSION,
description: `Dictionary in category '${categoryId}' not found`,
};
}
const schemasDictionary = {
name: currentDictionary.name,
version: currentDictionary.version,
schemas: currentDictionary.schemas,
};
// Validate entity name
const entitySchema = schemasDictionary.schemas.find((item) => item.name === entityName);
if (!entitySchema) {
return {
status: CREATE_SUBMISSION_STATUS.INVALID_SUBMISSION,
description: `Invalid entity name ${entityName} for submission`,
};
}
// Get Active Submission or Open a new one
const activeSubmission = await getOrCreateActiveSubmission({ categoryId, username, organization });
// Running Schema validation in the background do not need to wait
// Result of validations will be stored in database
validateRecordsAsync(records, {
categoryId,
organization,
schema: entitySchema,
username,
});
return {
status: CREATE_SUBMISSION_STATUS.PROCESSING,
description: 'Submission records are being processed',
submissionId: activeSubmission.id,
};
};
return {
commitSubmission,
deleteActiveSubmissionById,
deleteActiveSubmissionEntity,
getSubmissionsByCategory,
getSubmissionById,
getActiveSubmissionByOrganization,
getOrCreateActiveSubmission,
submit,
};
};
export default service;