"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.performUpdate = exports.executeUpdate = void 0; var _coreSavedObjectsServer = require("@kbn/core-saved-objects-server"); var _coreSavedObjectsUtilsServer = require("@kbn/core-saved-objects-utils-server"); var _coreSavedObjectsBaseServerInternal = require("@kbn/core-saved-objects-base-server-internal"); var _coreElasticsearchServerInternal = require("@kbn/core-elasticsearch-server-internal"); var _constants = require("../constants"); var _utils = require("../utils"); var _utils2 = require("./utils"); /* * 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. */ const performUpdate = async (updateParams, apiContext) => { const { type, id, options } = updateParams; const { allowedTypes, helpers } = apiContext; const namespace = helpers.common.getCurrentNamespace(options.namespace); // check request is valid const { validRequest, error } = (0, _utils.isValidRequest)({ allowedTypes, type, id }); if (!validRequest && error) { throw error; } const maxAttempts = options.version ? 1 : 1 + _constants.DEFAULT_RETRY_COUNT; // handle retryOnConflict manually by reattempting the operation in case of conflict errors let response; for (let currentAttempt = 1; currentAttempt <= maxAttempts; currentAttempt++) { try { response = await executeUpdate(updateParams, apiContext, { namespace }); break; } catch (e) { if (_coreSavedObjectsServer.SavedObjectsErrorHelpers.isConflictError(e) && e.retryableConflict && currentAttempt < maxAttempts) { continue; } throw e; } } return response; }; exports.performUpdate = performUpdate; const executeUpdate = async ({ id, type, attributes, options }, { registry, helpers, client, serializer, extensions = {}, logger }, { namespace }) => { var _preflightDocNSResult; const { common: commonHelper, encryption: encryptionHelper, preflight: preflightHelper, migration: migrationHelper, validation: validationHelper } = helpers; const { securityExtension } = extensions; const { version, references, upsert, refresh = _constants.DEFAULT_REFRESH_SETTING, migrationVersionCompatibility } = options; // Preflight calls to get the doc and check namespaces for multinamespace types. const preflightDocResult = await preflightHelper.preflightGetDocForUpdate({ type, id, namespace }); const preflightDocNSResult = preflightHelper.preflightCheckNamespacesForUpdate({ type, id, namespace, preflightDocResult }); const existingNamespaces = (_preflightDocNSResult = preflightDocNSResult === null || preflightDocNSResult === void 0 ? void 0 : preflightDocNSResult.savedObjectNamespaces) !== null && _preflightDocNSResult !== void 0 ? _preflightDocNSResult : []; const authorizationResult = await (securityExtension === null || securityExtension === void 0 ? void 0 : securityExtension.authorizeUpdate({ namespace, object: { type, id, existingNamespaces } })); // validate if an update (directly update or create the object instead) can be done, based on if the doc exists or not const docOutsideNamespace = (preflightDocNSResult === null || preflightDocNSResult === void 0 ? void 0 : preflightDocNSResult.checkResult) === 'found_outside_namespace'; const docNotFound = (preflightDocNSResult === null || preflightDocNSResult === void 0 ? void 0 : preflightDocNSResult.checkResult) === 'not_found' || preflightDocResult.checkDocFound === 'not_found'; // doc not in namespace, or doc not found but we're not upserting => throw 404 if (docOutsideNamespace || docNotFound && !upsert) { throw _coreSavedObjectsServer.SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } if (upsert && (preflightDocNSResult === null || preflightDocNSResult === void 0 ? void 0 : preflightDocNSResult.checkResult) === 'not_found') { // we only need to check multi-namespace objects. Single and agnostic types do not have aliases. // throws SavedObjectsErrorHelpers.createConflictError(type, id) if there is one await preflightHelper.preflightCheckForUpsertAliasConflict(type, id, namespace); } // migrate the existing doc to the current version let migrated; if (preflightDocResult.checkDocFound === 'found') { const document = (0, _utils2.getSavedObjectFromSource)(registry, type, id, preflightDocResult.rawDocSource, { migrationVersionCompatibility }); try { migrated = migrationHelper.migrateStorageDocument(document); } catch (migrateStorageDocError) { throw _coreSavedObjectsServer.SavedObjectsErrorHelpers.decorateGeneralError(migrateStorageDocError, 'Failed to migrate document to the latest version.'); } } // END ALL PRE_CLIENT CALL CHECKS && MIGRATE EXISTING DOC; const time = (0, _utils2.getCurrentTime)(); let updatedOrCreatedSavedObject; // `upsert` option set and document was not found -> we need to perform an upsert operation const shouldPerformUpsert = upsert && docNotFound; let savedObjectNamespace; let savedObjectNamespaces; if (namespace && registry.isSingleNamespace(type)) { savedObjectNamespace = namespace; } else if (registry.isMultiNamespace(type)) { savedObjectNamespaces = preflightDocNSResult.savedObjectNamespaces; } // UPSERT CASE START if (shouldPerformUpsert) { // ignore attributes if creating a new doc: only use the upsert attributes // don't include upsert if the object already exists; ES doesn't allow upsert in combination with version properties const migratedUpsert = migrationHelper.migrateInputDocument({ id, type, ...(savedObjectNamespace && { namespace: savedObjectNamespace }), ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), attributes: { ...(await encryptionHelper.optionallyEncryptAttributes(type, id, namespace, upsert)) }, updated_at: time, ...(Array.isArray(references) && { references }) }); validationHelper.validateObjectForCreate(type, migratedUpsert); const rawUpsert = serializer.savedObjectToRaw(migratedUpsert); const createRequestParams = { id: rawUpsert._id, index: commonHelper.getIndexForType(type), refresh, body: rawUpsert._source, ...(version ? (0, _coreSavedObjectsBaseServerInternal.decodeRequestVersion)(version) : {}), require_alias: true }; const { body: createDocResponseBody, statusCode, headers } = await client.create(createRequestParams, { meta: true }).catch(err => { if (_coreSavedObjectsServer.SavedObjectsErrorHelpers.isEsUnavailableError(err)) { throw err; } if (_coreSavedObjectsServer.SavedObjectsErrorHelpers.isNotFoundError(err)) { // see "404s from missing index" above throw _coreSavedObjectsServer.SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } if (_coreSavedObjectsServer.SavedObjectsErrorHelpers.isConflictError(err)) { // flag the error as being caused by an update conflict err.retryableConflict = true; } throw err; }); if ((0, _coreElasticsearchServerInternal.isNotFoundFromUnsupportedServer)({ statusCode, headers })) { throw _coreSavedObjectsServer.SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(id, type); } // client.create doesn't return the index document. // Use rawUpsert as the _source const upsertedSavedObject = serializer.rawToSavedObject({ ...rawUpsert, ...createDocResponseBody }, { migrationVersionCompatibility }); const { originId } = upsertedSavedObject !== null && upsertedSavedObject !== void 0 ? upsertedSavedObject : {}; let namespaces = []; if (!registry.isNamespaceAgnostic(type)) { var _upsertedSavedObject$; namespaces = (_upsertedSavedObject$ = upsertedSavedObject.namespaces) !== null && _upsertedSavedObject$ !== void 0 ? _upsertedSavedObject$ : [_coreSavedObjectsUtilsServer.SavedObjectsUtils.namespaceIdToString(upsertedSavedObject.namespace)]; } updatedOrCreatedSavedObject = { id, type, updated_at: time, version: (0, _coreSavedObjectsBaseServerInternal.encodeHitVersion)(createDocResponseBody), namespaces, ...(originId && { originId }), references, attributes: upsert // these ignore the attribute values provided in the main request body. }; // UPSERT CASE END } else { // UPDATE CASE START // at this point, we already know 1. the document exists 2. we're not doing an upsert // therefor we can safely process with the "standard" update sequence. const updatedAttributes = (0, _utils2.mergeForUpdate)({ ...migrated.attributes }, await encryptionHelper.optionallyEncryptAttributes(type, id, namespace, attributes)); const migratedUpdatedSavedObjectDoc = migrationHelper.migrateInputDocument({ ...migrated, id, type, // need to override the redacted NS values from the decrypted/migrated document namespace: savedObjectNamespace, namespaces: savedObjectNamespaces, attributes: updatedAttributes, updated_at: time, ...(Array.isArray(references) && { references }) }); const docToSend = serializer.savedObjectToRaw(migratedUpdatedSavedObjectDoc); // implement creating the call params const indexRequestParams = { id: docToSend._id, index: commonHelper.getIndexForType(type), refresh, body: docToSend._source, // using version from the source doc if not provided as option to avoid erasing changes in case of concurrent calls ...(0, _coreSavedObjectsBaseServerInternal.decodeRequestVersion)(version || migrated.version), require_alias: true }; const { body: indexDocResponseBody, statusCode, headers } = await client.index(indexRequestParams, { meta: true }).catch(err => { if (_coreSavedObjectsServer.SavedObjectsErrorHelpers.isEsUnavailableError(err)) { throw err; } if (_coreSavedObjectsServer.SavedObjectsErrorHelpers.isNotFoundError(err)) { // see "404s from missing index" above throw _coreSavedObjectsServer.SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } if (_coreSavedObjectsServer.SavedObjectsErrorHelpers.isConflictError(err)) { // flag the error as being caused by an update conflict err.retryableConflict = true; } throw err; }); // throw if we can't verify a 404 response is from Elasticsearch if ((0, _coreElasticsearchServerInternal.isNotFoundFromUnsupportedServer)({ statusCode, headers })) { throw _coreSavedObjectsServer.SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(id, type); } // client.index doesn't return the indexed document. // Rather than making another round trip to elasticsearch to fetch the doc, we use the SO we sent // rawToSavedObject adds references as [] if undefined const updatedSavedObject = serializer.rawToSavedObject({ ...docToSend, ...indexDocResponseBody }, { migrationVersionCompatibility }); const { originId } = updatedSavedObject !== null && updatedSavedObject !== void 0 ? updatedSavedObject : {}; let namespaces = []; if (!registry.isNamespaceAgnostic(type)) { var _updatedSavedObject$n; namespaces = (_updatedSavedObject$n = updatedSavedObject.namespaces) !== null && _updatedSavedObject$n !== void 0 ? _updatedSavedObject$n : [_coreSavedObjectsUtilsServer.SavedObjectsUtils.namespaceIdToString(updatedSavedObject.namespace)]; } updatedOrCreatedSavedObject = { id, type, updated_at: time, version: (0, _coreSavedObjectsBaseServerInternal.encodeHitVersion)(indexDocResponseBody), namespaces, ...(originId && { originId }), references, attributes }; } return encryptionHelper.optionallyDecryptAndRedactSingleResult(updatedOrCreatedSavedObject, authorizationResult === null || authorizationResult === void 0 ? void 0 : authorizationResult.typeMap, shouldPerformUpsert ? upsert : attributes); }; exports.executeUpdate = executeUpdate;