@overture-stack/lyric
Version:
Data Submission system
311 lines (310 loc) • 17.4 kB
JavaScript
import * as _ from 'lodash-es';
import categoryRepository from '../../repository/categoryRepository.js';
import submittedRepository from '../../repository/submittedRepository.js';
import { convertSqonToQuery } from '../../utils/convertSqonToQuery.js';
import { getDictionarySchemaRelations } from '../../utils/dictionarySchemaRelations.js';
import { filterUpdatesFromDeletes, mergeDeleteRecords } from '../../utils/submissionUtils.js';
import { fetchDataErrorResponse, getEntityNamesFromFilterOptions, transformmSubmittedDataToSubmissionDeleteData, } from '../../utils/submittedDataUtils.js';
import { CREATE_SUBMISSION_STATUS, VIEW_TYPE, } from '../../utils/types.js';
import processor from '../submission/processor.js';
import submissionService from '../submission/submission.js';
import searchDataRelations from './searchDataRelations.js';
import viewMode from './viewMode.js';
const PAGINATION_ERROR_MESSAGES = {
INVALID_CATEGORY_ID: 'Invalid Category ID',
NO_DATA_FOUND: 'No Submitted data found',
};
const submittedData = (dependencies) => {
const LOG_MODULE = 'SUBMITTED_DATA_SERVICE';
const submittedDataRepo = submittedRepository(dependencies);
const { logger } = dependencies;
const { convertRecordsToCompoundDocuments } = viewMode(dependencies);
const { searchDirectDependents } = searchDataRelations(dependencies);
const deleteSubmittedDataBySystemId = async (categoryId, systemId, username) => {
const { getSubmittedDataBySystemId } = submittedDataRepo;
const { getActiveDictionaryByCategory } = categoryRepository(dependencies);
const { getOrCreateActiveSubmission } = submissionService(dependencies);
const { performDataValidation } = processor(dependencies);
// get SubmittedData by SystemId
const foundRecordToDelete = await getSubmittedDataBySystemId(systemId);
if (!foundRecordToDelete) {
return {
status: CREATE_SUBMISSION_STATUS.INVALID_SUBMISSION,
description: `No Submitted data found with systemId '${systemId}'`,
inProcessEntities: [],
};
}
logger.info(LOG_MODULE, `Found Submitted Data with system ID '${systemId}'`);
if (foundRecordToDelete.dictionaryCategoryId !== categoryId) {
return {
status: CREATE_SUBMISSION_STATUS.INVALID_SUBMISSION,
description: `Invalid Category ID '${categoryId}' for system ID '${systemId}'`,
inProcessEntities: [],
};
}
// get current dictionary
const currentDictionary = await getActiveDictionaryByCategory(categoryId);
if (!currentDictionary) {
return {
status: CREATE_SUBMISSION_STATUS.INVALID_SUBMISSION,
description: `Dictionary not found`,
inProcessEntities: [],
};
}
// get dictionary relations
const dictionaryRelations = getDictionarySchemaRelations(currentDictionary.schemas);
const recordDependents = await searchDirectDependents({
data: foundRecordToDelete.data,
dictionaryRelations,
entityName: foundRecordToDelete.entityName,
organization: foundRecordToDelete.organization,
systemId: foundRecordToDelete.systemId,
});
logger.info(LOG_MODULE, `Found ${recordDependents.length} dependendencies on systemId '${systemId}'`);
const submittedDataToDelete = [foundRecordToDelete, ...recordDependents];
const recordsToDeleteMap = transformmSubmittedDataToSubmissionDeleteData(submittedDataToDelete);
// Get Active Submission or Open a new one
const activeSubmission = await getOrCreateActiveSubmission({
categoryId: foundRecordToDelete.dictionaryCategoryId,
username,
organization: foundRecordToDelete.organization,
});
// Merge current Active Submission delete entities with unique records to delete based on systemId
const mergedSubmissionDeletes = mergeDeleteRecords(activeSubmission.data.deletes || {}, recordsToDeleteMap);
const entitiesToProcess = Object.keys(mergedSubmissionDeletes);
// filter out update records found matching systemID on delete records
const filteredUpdates = filterUpdatesFromDeletes(activeSubmission.data.updates ?? {}, mergedSubmissionDeletes);
// Validate and update Active Submission
performDataValidation({
originalSubmission: activeSubmission,
submissionData: {
inserts: activeSubmission.data.inserts,
updates: filteredUpdates,
deletes: mergedSubmissionDeletes,
},
username,
});
logger.info(LOG_MODULE, `Added '${entitiesToProcess.length}' records to be deleted on the Active Submission`);
return {
status: CREATE_SUBMISSION_STATUS.PROCESSING,
description: 'Submission data is being processed',
submissionId: activeSubmission.id.toString(),
inProcessEntities: entitiesToProcess,
};
};
const editSubmittedData = async ({ categoryId, entityName, organization, records, username, }) => {
logger.info(LOG_MODULE, `Processing '${records.length}' records on category id '${categoryId}' organization '${organization}'`);
const { getActiveDictionaryByCategory } = categoryRepository(dependencies);
const { getOrCreateActiveSubmission } = submissionService(dependencies);
const { processEditRecordsAsync } = 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
processEditRecordsAsync(records, {
submission: activeSubmission,
schema: entitySchema,
username,
});
return {
status: CREATE_SUBMISSION_STATUS.PROCESSING,
description: 'Submission records are being processed',
submissionId: activeSubmission.id,
};
};
/**
* Fetches submitted data from the database based on the provided category ID, pagination options, and filter options.
*
* This function retrieves a list of submitted data associated with the specified `categoryId`.
* It also supports pagination, view representation and filtering based on entity names or a compound condition.
* The returned data includes both the retrieved records and metadata about the total number of records.
*
* @param categoryId - The ID of the category for which data is being fetched.
* @param paginationOptions - An object containing pagination options, such as page number and items per page.
* @param filterOptions - An object containing options for filtering the data.
* @param filterOptions.entityName - An optional array of entity names to filter the data by.
* @param filterOptions.view - An optional flag indicating the view type
* @returns A promise that resolves to an object containing:
* - `result`: An array of `SubmittedDataResponse` objects, representing the fetched data.
* - `metadata`: An object containing metadata about the fetched data, including the total number of records.
* If an error occurs during data retrieval, `metadata` will include an `errorMessage` property.
*/
const getSubmittedDataByCategory = async (categoryId, paginationOptions, filterOptions) => {
const { getSubmittedDataByCategoryIdPaginated, getTotalRecordsByCategoryId } = submittedDataRepo;
const { getCategoryById } = categoryRepository(dependencies);
const category = await getCategoryById(categoryId);
if (!category?.activeDictionary) {
return fetchDataErrorResponse(PAGINATION_ERROR_MESSAGES.INVALID_CATEGORY_ID);
}
const defaultCentricEntity = category.defaultCentricEntity || undefined;
let recordsPaginated = await getSubmittedDataByCategoryIdPaginated(categoryId, paginationOptions, {
entityNames: getEntityNamesFromFilterOptions(filterOptions, defaultCentricEntity),
});
if (recordsPaginated.length === 0) {
return fetchDataErrorResponse(PAGINATION_ERROR_MESSAGES.NO_DATA_FOUND);
}
if (filterOptions.view === VIEW_TYPE.Values.compound) {
recordsPaginated = await convertRecordsToCompoundDocuments({
dictionary: category.activeDictionary.dictionary,
records: recordsPaginated,
defaultCentricEntity: defaultCentricEntity,
});
}
const totalRecords = await getTotalRecordsByCategoryId(categoryId, {
entityNames: getEntityNamesFromFilterOptions(filterOptions, defaultCentricEntity),
});
logger.info(LOG_MODULE, `Retrieved '${recordsPaginated.length}' Submitted data on categoryId '${categoryId}'`);
return {
result: recordsPaginated,
metadata: {
totalRecords,
},
};
};
/**
* Fetches submitted data from the database based on the provided category ID, organization, pagination options, and optional filter options.
*
* This function retrieves a list of submitted data associated with the specified `categoryId` and `organization`.
* It supports a view representation, pagination and optional filtering using a structured query (`sqon`) or entity names.
* The result includes both the fetched data and metadata such as the total number of records and an error message if applicable.
*
* @param categoryId - The ID of the category for which data is being fetched.
* @param organization - The name of the organization to filter the data by.
* @param paginationOptions - An object containing pagination options, such as page number and items per page.
* @param filterOptions - Optional filtering options.
* @param filterOptions.sqon - An optional Structured Query Object Notation (SQON) for advanced filtering criteria.
* @param filterOptions.entityName - An optional array of entity names to filter the data by. Can include undefined entries.
* @param filterOptions.view - An optional flag indicating the view type
* @returns A promise that resolves to an object containing:
* - `result`: An array of `SubmittedDataResponse` objects, representing the fetched data.
* - `metadata`: An object containing metadata about the fetched data, including the total number of records.
* If an error occurs during data retrieval, `metadata` will include an `errorMessage` property.
*/
const getSubmittedDataByOrganization = async (categoryId, organization, paginationOptions, filterOptions) => {
const { getSubmittedDataByCategoryIdAndOrganizationPaginated, getTotalRecordsByCategoryIdAndOrganization } = submittedDataRepo;
const { getCategoryById } = categoryRepository(dependencies);
const category = await getCategoryById(categoryId);
if (!category?.activeDictionary) {
return fetchDataErrorResponse(PAGINATION_ERROR_MESSAGES.INVALID_CATEGORY_ID);
}
const defaultCentricEntity = category.defaultCentricEntity || undefined;
const sqonQuery = convertSqonToQuery(filterOptions?.sqon);
let recordsPaginated = await getSubmittedDataByCategoryIdAndOrganizationPaginated(categoryId, organization, paginationOptions, {
sql: sqonQuery,
entityNames: getEntityNamesFromFilterOptions(filterOptions, defaultCentricEntity),
});
if (recordsPaginated.length === 0) {
return fetchDataErrorResponse(PAGINATION_ERROR_MESSAGES.NO_DATA_FOUND);
}
if (filterOptions.view === VIEW_TYPE.Values.compound) {
recordsPaginated = await convertRecordsToCompoundDocuments({
dictionary: category.activeDictionary.dictionary,
records: recordsPaginated,
defaultCentricEntity: defaultCentricEntity,
});
}
const totalRecords = await getTotalRecordsByCategoryIdAndOrganization(categoryId, organization, {
sql: sqonQuery,
entityNames: getEntityNamesFromFilterOptions(filterOptions, defaultCentricEntity),
});
logger.info(LOG_MODULE, `Retrieved '${recordsPaginated.length}' Submitted data on categoryId '${categoryId}' organization '${organization}'`);
return {
result: recordsPaginated,
metadata: {
totalRecords,
},
};
};
/**
* Fetches submitted data from the database based on the specified category ID and system ID.
*
* This function retrieves the submitted data associated with a given `categoryId` and `systemId`.
* It supports a view representation defined by the `filterOptions`. The result includes both the
* fetched data and metadata, including an error message if applicable.
*
* @param categoryId - The ID of the category for which data is being fetched.
* @param systemId - The unique identifier for the system associated with the submitted data.
* @param filterOptions - An object containing options for data representation.
* @param filterOptions.view - The desired view type for the data representation, such as 'flat' or 'compound'.
* @returns A promise that resolves to an object containing:
* - `result`: The fetched `SubmittedDataResponse`, or `undefined` if no data is found.
* - `metadata`: An object containing metadata about the fetched data, including an optional `errorMessage` property.
*/
const getSubmittedDataBySystemId = async (categoryId, systemId, filterOptions) => {
// get SubmittedData by SystemId
const foundRecord = await submittedDataRepo.getSubmittedDataBySystemId(systemId);
logger.info(LOG_MODULE, `Found Submitted Data with system ID '${systemId}'`);
if (!foundRecord) {
return { result: undefined, metadata: { errorMessage: `No Submitted data found with systemId '${systemId}'` } };
}
if (foundRecord.dictionaryCategoryId !== categoryId) {
return {
result: undefined,
metadata: { errorMessage: `Invalid Category ID '${categoryId}' for system ID '${systemId}'` },
};
}
let recordResponse = {
data: foundRecord.data,
entityName: foundRecord.entityName,
isValid: foundRecord.isValid,
organization: foundRecord.organization,
systemId: foundRecord.systemId,
};
if (filterOptions.view === VIEW_TYPE.Values.compound) {
const { getCategoryById } = categoryRepository(dependencies);
const category = await getCategoryById(foundRecord.dictionaryCategoryId);
if (!category?.activeDictionary) {
return { result: undefined, metadata: { errorMessage: `Invalid Category ID` } };
}
const defaultCentricEntity = category.defaultCentricEntity || undefined;
// Convert to compound records if the record matches the default centric entity type.
// If no default centric entity is defined, the record's entity type will be used
if (!defaultCentricEntity || defaultCentricEntity === foundRecord.entityName) {
const [convertedRecord] = await convertRecordsToCompoundDocuments({
dictionary: category.activeDictionary.dictionary,
records: [recordResponse],
defaultCentricEntity: defaultCentricEntity,
});
recordResponse = convertedRecord;
}
}
return {
result: recordResponse,
metadata: {},
};
};
return {
deleteSubmittedDataBySystemId,
editSubmittedData,
getSubmittedDataByCategory,
getSubmittedDataByOrganization,
getSubmittedDataBySystemId,
};
};
export default submittedData;