@overture-stack/lyric
Version:
Data Submission system
233 lines (232 loc) • 12.4 kB
JavaScript
import createAuditRepository from '../repository/auditRepository.js';
import createCategoryRepository from '../repository/categoryRepository.js';
import createMigrationRepository from '../repository/dictionaryMigrationRepository.js';
import createSubmittedDataRepository from '../repository/submittedRepository.js';
import { failure, success } from '../utils/result.js';
import submissionProcessorFactory from './submission/submissionProcessor.js';
import submissionService from './submission/submissionService.js';
const migrationService = (dependencies) => {
const LOG_MODULE = 'MIGRATION_SERVICE';
const { logger, onFinishCommit } = dependencies;
const migrationRepository = createMigrationRepository(dependencies);
const auditRepository = createAuditRepository(dependencies);
const categoryRepository = createCategoryRepository(dependencies);
const submittedDataRepository = createSubmittedDataRepository(dependencies);
const submissionProcessor = submissionProcessorFactory.create(dependencies);
/**
* Update the status of the migration to COMPLETED or FAILED
* @param migrationId The ID of the migration to finalize
* @param status The final status of the migration, either 'COMPLETED' or 'FAILED'
* @param userName The name of the user that is finalizing the migration (for audit purposes)
* @returns A Result object with null data and a string error message in case of failure
*/
const finalizeMigration = async ({ migrationId, status, userName, }) => {
try {
const resultUpdate = await migrationRepository.update(migrationId, {
status,
updatedAt: new Date(),
updatedBy: userName,
});
if (resultUpdate === 0) {
logger.info(LOG_MODULE, `Migration with id '${migrationId}' not found for finalization`);
return failure(`Migration with id '${migrationId}' not found.`);
}
logger.info(LOG_MODULE, `Migration finalized for migrationId '${migrationId}' with status '${status}'`);
return success(null);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(LOG_MODULE, `Error finalizing migration for migrationId '${migrationId}'`, errorMessage);
return failure(`Error finalizing migration for migrationId '${migrationId}'`);
}
};
/**
* Find the active migration by category ID
* @param categoryId The ID of the category to find the active migration for
* @returns The active migration for the given category ID, or null if no active migration exists
* @throws Will throw an error if there is an issue retrieving the migration from the repository
*/
const getActiveMigrationByCategoryId = async (categoryId) => {
const migrations = await migrationRepository.getMigrationsByCategoryId(categoryId, { page: 1, pageSize: 1 }, { status: 'IN_PROGRESS' });
const activeMigration = migrations.result[0];
if (activeMigration) {
logger.info(LOG_MODULE, `Active migration found for categoryId '${categoryId}'`);
return activeMigration;
}
else {
logger.info(LOG_MODULE, `No active migration for categoryId '${categoryId}'`);
return null;
}
};
/**
* Find a migration by its ID
* @param migrationId The ID of the migration to find
* @returns The migration with the given ID, or null if no migration exists
* @throws Will throw an error if there is an issue retrieving the migration from the repository
*/
const getMigrationById = async (migrationId) => {
const migration = await migrationRepository.getMigrationById(migrationId);
if (!migration) {
logger.info(LOG_MODULE, `No migration found for migrationId '${migrationId}'`);
return null;
}
logger.info(LOG_MODULE, `Migration found for migrationId '${migrationId}'`);
const invalidRecords = await auditRepository.getTotalRecordsByCategoryIdAndOrganization(migration.category.id, {
newIsValid: false,
submissionId: migration.submissionId,
});
return { ...migration, invalidRecords };
};
/**
* Find migrations by category ID with pagination options
* @param categoryId The ID of the category to find migrations for
* @param paginationOptions The pagination options to apply
* @returns An object containing the metadata and the list of migrations
* @throws Will throw an error if there is an issue retrieving the migrations from the repository
*/
const getMigrationsByCategoryId = async (categoryId, paginationOptions) => {
const migrationsOnCategory = await migrationRepository.getMigrationsByCategoryId(categoryId, paginationOptions, {});
const resultsWithInvalidRecords = await Promise.all(migrationsOnCategory.result.map(async (migrationSummary) => {
const invalidRecords = await auditRepository.getTotalRecordsByCategoryIdAndOrganization(categoryId, {
submissionId: migrationSummary.submissionId,
});
return { ...migrationSummary, invalidRecords };
}));
return {
metadata: {
totalRecords: migrationsOnCategory.metadata.totalRecords,
},
result: resultsWithInvalidRecords,
};
};
/**
* Retrieve the records impacted by a migration.
* Accepts pagination options and filters by entity names, organizations and validity of the records.
* @param migrationId The ID of the migration to retrieve records for
* @param options The options to filter and paginate the records
* @returns A paginated result of migration audit records
* @throws Will throw an error if there is an issue retrieving the records from the repository
*/
const getMigrationRecords = async (migrationId, options) => {
const migrationRecords = await migrationRepository.getMigrationAuditRecords(migrationId, options);
logger.info(LOG_MODULE, `Migration records retrieved for migrationId '${migrationId}' with options '${JSON.stringify(options)}'`);
return migrationRecords;
};
/**
* Creates a Migration record or update retries if one exists.
* Then, it starts running migration in a worker thread
* @param param0 The parameters for initiating the migration
* @returns The result of the migration initiation process, with the migrationId in case of success or
* an error message in case of failure
*/
const initiateMigration = async ({ categoryId, fromDictionaryId, toDictionaryId, userName, }) => {
const { getOrCreateActiveSubmission } = submissionService(dependencies);
try {
const findMigrationResult = await migrationRepository.getMigrationsByCategoryId(categoryId, { page: 1, pageSize: 1 }, { fromDictionaryId, toDictionaryId });
let migrationId;
let submissionId;
// Migration already exists, update retries count
const existingMigration = findMigrationResult.result[0];
if (existingMigration) {
const updatedRetriesCount = existingMigration.retries + 1;
migrationId = await migrationRepository.update(existingMigration.id, {
retries: updatedRetriesCount,
updatedBy: userName,
updatedAt: new Date(),
});
logger.info(LOG_MODULE, `Existing migration found for categoryId '${categoryId}'. Incremented retries to ${updatedRetriesCount}`);
}
else {
// Create new migration record
submissionId = await getOrCreateActiveSubmission({
categoryId,
organization: '',
username: userName,
});
const newMigration = {
categoryId,
fromDictionaryId,
toDictionaryId,
submissionId,
status: 'IN_PROGRESS',
createdBy: userName,
createdAt: new Date(),
};
migrationId = await migrationRepository.save(newMigration);
logger.info(LOG_MODULE, `Creating migration record for categoryId '${categoryId}'`);
}
// Perform dictionary migration in a worker thread
dependencies.workerPool.dictionaryMigration({
migrationId,
userName,
});
logger.info(LOG_MODULE, `Migration initiated for categoryId '${categoryId}'`);
return success(migrationId);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(LOG_MODULE, `Error initiating migration for categoryId '${categoryId}'`, errorMessage);
return failure(`Error initiating migration for categoryId '${categoryId}'`);
}
};
/**
* **This function is designed to be executed in a worker thread.**
* Performs the Submitted data validation for a given migration,
* it iterates over all organizations and validates the submitted data for each of them.
* @param migrationId The ID of the migration to perform
* @param userName The name of the user that initiated the migration (for audit purposes)
* @returns A Result object with null data and a string error message in case of failure
*/
const performMigrationValidation = async ({ migrationId, userName, }) => {
const { getAllOrganizationsByCategoryId, getSubmittedDataByCategoryIdAndOrganization } = submittedDataRepository;
const { getActiveDictionaryByCategory } = categoryRepository;
const { performCommitSubmissionAsync } = submissionProcessor;
try {
const migration = await migrationRepository.getMigrationById(migrationId);
if (!migration) {
return failure(`Migration with id '${migrationId}' not found`);
}
const { category, submissionId } = migration;
const dictionary = await getActiveDictionaryByCategory(category.id);
if (!dictionary) {
return failure(`Dictionary in category '${category.id}' not found`);
}
const organizations = await getAllOrganizationsByCategoryId(category.id);
logger.info(LOG_MODULE, `Starting migration validation for following organizations '${organizations}'`);
for (const organization of organizations) {
const submittedDataToValidate = await getSubmittedDataByCategoryIdAndOrganization(category.id, organization);
logger.info(LOG_MODULE, `Performing migration validation for organization '${organization}' with ${submittedDataToValidate.length} submitted records`);
await performCommitSubmissionAsync({
dataToValidate: {
inserts: [],
submittedData: submittedDataToValidate,
deletes: [],
updates: {},
},
submissionId,
dictionary,
username: userName,
isMigration: true,
onFinishCommit,
});
}
logger.info(LOG_MODULE, `Migration validation completed for submissionId '${submissionId}'`);
return success(null);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(LOG_MODULE, `Error performing migration validation for migrationId '${migrationId}'`, errorMessage);
return failure(`Error performing migration validation for migrationId '${migrationId}'`);
}
};
return {
finalizeMigration,
getActiveMigrationByCategoryId,
getMigrationsByCategoryId,
getMigrationById,
getMigrationRecords,
initiateMigration,
performMigrationValidation,
};
};
export default migrationService;