UNPKG

@overture-stack/lyric

Version:
314 lines (313 loc) 15.2 kB
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;