"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.resolveImportErrors = resolveImportErrors; /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0 and the Server Side Public License, v 1; you may not use this file except * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ // the HTTP route requires type and ID; all other field are optional const RESOLVABLE_ERRORS = ['conflict', 'ambiguous_conflict', 'missing_references']; const isConflict = failure => failure.error.type === 'conflict'; const isAmbiguousConflict = failure => failure.error.type === 'ambiguous_conflict'; const isAnyConflict = failure => isConflict(failure) || isAmbiguousConflict(failure); /** * The server-side code was updated to include missing_references errors and conflict/ambiguous_conflict errors for the same object in the * same response. This client-side code was not built to handle multiple errors for a single import object, though. We simply filter out any * conflicts if a missing_references error for the same object is present. This means that the missing_references error will get resolved * or skipped first, and any conflicts still present will be returned again and resolved with another API call. */ const filterFailedImports = failures => { const missingReferences = failures.filter(({ error: { type } }) => type === 'missing_references').reduce((acc, { obj: { type, id } }) => acc.add(`${type}:${id}`), new Set()); return failures.filter(failure => !isAnyConflict(failure) || isAnyConflict(failure) && !missingReferences.has(`${failure.obj.type}:${failure.obj.id}`)); }; async function callResolveImportErrorsApi(http, file, retries, createNewCopies) { const formData = new FormData(); formData.append('file', file); formData.append('retries', JSON.stringify(retries)); const query = createNewCopies ? { createNewCopies } : {}; return http.post('/api/saved_objects/_resolve_import_errors', { headers: { // Important to be undefined, it forces proper headers to be set for FormData 'Content-Type': undefined }, body: formData, query }); } function mapImportFailureToRetryObject({ failure, retryDecisionCache, replaceReferencesCache, state }) { const { unmatchedReferences = [] } = state; const retryDecision = retryDecisionCache.get(`${failure.obj.type}:${failure.obj.id}`); // Conflicts without a resolution are skipped if (!(retryDecision !== null && retryDecision !== void 0 && retryDecision.retry) && (failure.error.type === 'conflict' || failure.error.type === 'ambiguous_conflict')) { return; } // Replace references if user chose a new reference if (failure.error.type === 'missing_references') { const objReplaceReferences = replaceReferencesCache.get(`${failure.obj.type}:${failure.obj.id}`) || []; const indexPatternRefs = failure.error.references.filter(obj => obj.type === 'index-pattern'); for (const reference of indexPatternRefs) { for (const { existingIndexPatternId: from, newIndexPatternId: to } of unmatchedReferences) { const matchesIndexPatternId = from === reference.id; if (!to || !matchesIndexPatternId) { continue; } const type = 'index-pattern'; if (!objReplaceReferences.some(ref => ref.type === type && ref.from === from && ref.to === to)) { objReplaceReferences.push({ type, from, to }); } } } replaceReferencesCache.set(`${failure.obj.type}:${failure.obj.id}`, objReplaceReferences); // Skip if nothing to replace, the UI option selected would be --Skip Import-- if (objReplaceReferences.length === 0) { return; } } return { id: failure.obj.id, type: failure.obj.type, ...((retryDecision === null || retryDecision === void 0 ? void 0 : retryDecision.retry) && retryDecision.options), replaceReferences: replaceReferencesCache.get(`${failure.obj.type}:${failure.obj.id}`) }; } async function resolveImportErrors({ http, getConflictResolutions, state }) { const retryDecisionCache = new Map(); const replaceReferencesCache = new Map(); let { importCount, failedImports = [], successfulImports = [] } = state; const { file, importMode: { createNewCopies, overwrite: isOverwriteAllChecked } } = state; const doesntHaveRetryDecision = ({ obj }) => { return !retryDecisionCache.has(`${obj.type}:${obj.id}`); }; const getRetryDecision = ({ obj }) => { return retryDecisionCache.get(`${obj.type}:${obj.id}`); }; const callMapImportFailure = failure => mapImportFailureToRetryObject({ failure, retryDecisionCache, replaceReferencesCache, state }); const isNotSkipped = failure => { var _getRetryDecision; const { type } = failure.error; return !RESOLVABLE_ERRORS.includes(type) || ((_getRetryDecision = getRetryDecision(failure)) === null || _getRetryDecision === void 0 ? void 0 : _getRetryDecision.retry); }; // Loop until all issues are resolved while (failedImports.some(failure => RESOLVABLE_ERRORS.includes(failure.error.type))) { // Filter out multiple errors for the same object const filteredFailures = filterFailedImports(failedImports); // Resolve regular conflicts if (isOverwriteAllChecked) { filteredFailures.filter(isConflict).forEach(({ obj: { type, id }, error: { destinationId } }) => retryDecisionCache.set(`${type}:${id}`, { retry: true, options: { overwrite: true, ...(destinationId && { destinationId }) } })); } // prompt the user for each conflict const result = await getConflictResolutions(isOverwriteAllChecked ? filteredFailures.filter(isAmbiguousConflict).filter(doesntHaveRetryDecision) : filteredFailures.filter(isAnyConflict).filter(doesntHaveRetryDecision)); for (const key of Object.keys(result)) { retryDecisionCache.set(key, result[key]); } // Build retries array const failRetries = filteredFailures.map(callMapImportFailure).filter(obj => !!obj); const successRetries = successfulImports.map(({ type, id, overwrite, destinationId, createNewCopy }) => { const replaceReferences = replaceReferencesCache.get(`${type}:${id}`); return { type, id, ...(overwrite && { overwrite }), ...(replaceReferences && { replaceReferences }), destinationId, createNewCopy }; }); const retries = [...failRetries, ...successRetries]; // Scenario where there were no success results, all errors were skipped, and nothing to retry if (retries.length === 0) { // Cancelled overwrites aren't failures anymore failedImports = filteredFailures.filter(isNotSkipped); break; } // Call API const response = await callResolveImportErrorsApi(http, file, retries, createNewCopies); importCount = response.successCount; // reset the success count since we retry all successful results each time failedImports = []; for (const { error, ...obj } of response.errors || []) { failedImports.push({ error, obj }); } successfulImports = response.successResults || []; } return { status: 'success', importCount, failedImports, successfulImports }; }