UNPKG

@natlibfi/melinda-record-match-validator

Version:

Validates if two records matched by melinda-record-matching can be merged and sets merge priority

161 lines (132 loc) 8.42 kB
import createDebugLogger from 'debug'; import {comparisonTasksTable} from './comparisonTasks'; import {check984} from './field984'; import {nvdebug} from './utils'; import {MarcRecord} from '@natlibfi/marc-record'; const debug = createDebugLogger('@natlibfi/melinda-record-match-validator:index'); const debugDev = debug.extend('dev'); //const debugData = debug.extend('data'); // Apply some recursion evilness/madness/badness to perform only the tests we really really really want. function runComparisonTasks({nth, record1, record2, checkPreference = true, record1External = {}, record2External = {}, returnAll = false, comparisonTasks = comparisonTasksTable.recordImport}) { // DEVELOP: We could skip those tasks that are !validation if !checkPreference - but how? const currResult = comparisonTasks[nth].function({record1, record2, checkPreference, record1External, record2External}); // NB! Aborts after the last task or after a failure (meaning currResult === false)! No further tests are performed. Recursion means optimization :D debugDev(`Running task ${nth} (${comparisonTasks[nth].name}) - returnAll: ${returnAll}`); if (nth === comparisonTasks.length - 1 || (!returnAll && currResult === false)) { // eslint-disable-line no-extra-parens return [currResult]; } return [currResult].concat(runComparisonTasks({nth: nth + 1, record1, record2, checkPreference, record1External, record2External, returnAll, comparisonTasks})); } function makeComparisons({record1, record2, checkPreference = true, record1External = {}, record2External = {}, returnAll = false, comparisonTasks = comparisonTasksTable.recordImport}) { debugDev(`returnAll: ${returnAll}`); // Start with sanity check(s): if there are no tasks, it is not a failure: if (comparisonTasks.length === 0) { const resultForZeroTasks = {result: true, reason: `No rules defined`}; if (returnAll) { return [resultForZeroTasks]; } return resultForZeroTasks; } // Get results (if not returnAll, just up to the point of first failure): const results = runComparisonTasks({nth: 0, record1, record2, checkPreference, record1External, record2External, returnAll, comparisonTasks}); return returnAll ? returnAllResults() : returnDecisionPointResult(); // return array of all comparison task results function returnAllResults() { const allResults = results.map((result, i) => ({ result, reason: comparisonTasks[i].name, preference: comparisonTasks[i].preference, validation: comparisonTasks[i].validation, level: comparisonTasks[i].manual === undefined ? 'error' : comparisonTasks[i].manual, // eslint-disable-next-line camelcase validation_message_fi: comparisonTasks[i].validation_message_fi, // eslint-disable-next-line camelcase preference_message_fi: comparisonTasks[i].preference_message_fi })); if (checkPreference) { // Add f984 overide result (check preference override from records f984) const field984OverrideResult = getField984OverrideResult(); if (field984OverrideResult) { return allResults.concatenate(field984OverrideResult); } return allResults; } return allResults; } // return result from the decision point where first task fails function returnDecisionPointResult() { // If we do not want all results, if any of tests fails, return false and description for failing test if (results.length < comparisonTasks.length || results[results.length - 1] === false) { nvdebug(`makeComparisons() failed. Reason: ${comparisonTasks[results.length - 1].description}. (TEST: ${results.length}/${comparisonTasks.length})`, debugDev); return {result: false, reason: `${comparisonTasks[results.length - 1].description} failed`}; } if (!checkPreference) { // This will also skip separate field 984 check return {result: true, reason: 'all tests passed'}; } // Let's do extra check for preference override in records const field984OverrideResult = getField984OverrideResult(); if (field984OverrideResult) { return field984OverrideResult; } const decisionPoint = results.findIndex(val => val !== true && val !== false); if (decisionPoint === -1) { return {result: true, reason: 'both records passed all tests, but no winner was found'}; } return {result: results[decisionPoint], reason: `${results[decisionPoint]} won ${comparisonTasks[decisionPoint].description}`}; } // We get a separate override result, because normal preference checks find first preference function getField984OverrideResult() { const field984Override = check984({record1, record2}); if (field984Override === 'A' || field984Override === 'B') { return {result: field984Override, reason: 'Field 984 override applied (MRA-744)'}; } return; } } // record1External/record2External includes external information for record (for example whether it is an incomingRecord or databaseRecord) // MergeUI is currently used for manual merging of two database records // Returns array of failure responses, empty array if matchValidator does not return failures // [{ "result": "error/warning", "type": "validation/preference", "message": "finnish message"}] export function matchValidationForMergeUi({record1Object, record2Object, checkPreference = true, record1External = {'recordSource': 'databaseRecord'}, record2External = {'recordSource': 'databaseRecord'}, manual = true, comparisonTasks = comparisonTasksTable.humanMerge}) { debugDev(`Manual ${manual} (for Merge UI) - we have ${comparisonTasks.length} comparison tasks`); // Create MarcRecords here to avoid problems with differing MarcRecord versions etc. const record1 = new MarcRecord(record1Object, {subfieldValues: false}); const record2 = new MarcRecord(record2Object, {subfieldValues: false}); const result = makeComparisons({record1, record2, checkPreference, record1External, record2External, returnAll: true, comparisonTasks}); debugDev(JSON.stringify(result)); // return result-array failed results const resultForMergeUi = filterResultsForMergeUI(result); return resultForMergeUi; // Return to Merge UI only results that require action, ie. those that fail merge or change preference // MergeUI sends records non-preferred record as record1 and preferred record as record2, so preference result 'A' is a warning function filterResultsForMergeUI(allResults) { const failure = allResults .filter(r => r.result !== true) // Filter out passed tests .filter(r => r.result !== 'B'); // Filter out passed preference tests (record2/B is preferred) debugDev(`MatchValidator failed: ${JSON.stringify(failure, null, 4)}`); const messages = failure // eslint-disable-next-line camelcase .map(({result, level, validation_message_fi, preference_message_fi}) => ({ result: result === 'A' ? 'warning' : level, // all preference-results are warning in UI type: result === 'A' ? 'preference' : 'validation', // // eslint-disable-next-line camelcase message: result === 'A' ? preference_message_fi : validation_message_fi })); // Convert to messages debugDev(`MatchValidator results for MergeUI: ${JSON.stringify(messages, null, 4)}`); return messages; } } // record1External/record2External includes external information for record (for example whether it is an incomingRecord or databaseRecord) export default ({record1Object, record2Object, checkPreference = true, record1External = {}, record2External = {}, manual = false, comparisonTasks = comparisonTasksTable.recordImport}) => { debugDev(`Default (manual: ${manual}) (for record import) we have ${comparisonTasks.length} comparison tasks`); // Create MarcRecords here to avoid problems with differing MarcRecord versions etc. const record1 = new MarcRecord(record1Object, {subfieldValues: false}); const record2 = new MarcRecord(record2Object, {subfieldValues: false}); const result = makeComparisons({record1, record2, checkPreference, record1External, record2External, comparisonTasks}); debug(`Comparison result: ${result.result}, reason: ${result.reason}`); if (result.result === false) { return {action: false, preference: false, message: result.reason}; } return {action: 'merge', preference: {'name': result.reason, 'value': result.result}}; };