"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.buildActiveMappings = buildActiveMappings;
exports.diffMappings = diffMappings;
exports.getBaseMappings = getBaseMappings;
exports.getUpdatedHashes = void 0;
var _crypto = _interopRequireDefault(require("crypto"));
var _lodash = require("lodash");
/*
 * 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.
 */

/*
 * This file contains logic to build and diff the index mappings for a migration.
 */

/**
 * Creates an index mapping with the core properties required by saved object
 * indices, as well as the specified additional properties.
 *
 * @param typeDefinitions - the type definitions to build mapping from.
 */
function buildActiveMappings(typeDefinitions) {
  const mapping = getBaseMappings();
  const mergedProperties = validateAndMerge(mapping.properties, typeDefinitions);
  return (0, _lodash.cloneDeep)({
    ...mapping,
    properties: mergedProperties,
    _meta: {
      migrationMappingPropertyHashes: md5Values(mergedProperties)
    }
  });
}

/**
 * Diffs the actual vs expected mappings. The properties are compared using md5 hashes stored in _meta, because
 * actual and expected mappings *can* differ, but if the md5 hashes stored in actual._meta.migrationMappingPropertyHashes
 * match our expectations, we don't require a migration. This allows ES to tack on additional mappings that Kibana
 * doesn't know about or expect, without triggering continual migrations.
 */
function diffMappings(actual, expected) {
  var _actual$_meta;
  if (actual.dynamic !== expected.dynamic) {
    return {
      changedProp: 'dynamic'
    };
  }
  if (!((_actual$_meta = actual._meta) !== null && _actual$_meta !== void 0 && _actual$_meta.migrationMappingPropertyHashes)) {
    return {
      changedProp: '_meta'
    };
  }
  const changedProp = findChangedProp(actual._meta.migrationMappingPropertyHashes, expected._meta.migrationMappingPropertyHashes);
  return changedProp ? {
    changedProp: `properties.${changedProp}`
  } : undefined;
}

/**
 * Compares the actual vs expected mappings' hashes.
 * Returns a list with all the hashes that have been updated.
 */
const getUpdatedHashes = ({
  actual,
  expected
}) => {
  var _actual$_meta2;
  if (!((_actual$_meta2 = actual._meta) !== null && _actual$_meta2 !== void 0 && _actual$_meta2.migrationMappingPropertyHashes)) {
    return Object.keys(expected._meta.migrationMappingPropertyHashes);
  }
  const updatedHashes = Object.keys(expected._meta.migrationMappingPropertyHashes).filter(key => actual._meta.migrationMappingPropertyHashes[key] !== expected._meta.migrationMappingPropertyHashes[key]);
  return updatedHashes;
};

// Convert an object to an md5 hash string, using a stable serialization (canonicalStringify)
exports.getUpdatedHashes = getUpdatedHashes;
function md5Object(obj) {
  return _crypto.default.createHash('md5').update(canonicalStringify(obj)).digest('hex');
}

// JSON.stringify is non-canonical, meaning the same object may produce slightly
// different JSON, depending on compiler optimizations (e.g. object keys
// are not guaranteed to be sorted). This function consistently produces the same
// string, if passed an object of the same shape. If the outpuf of this function
// changes from one release to another, migrations will run, so it's important
// that this function remains stable across releases.
function canonicalStringify(obj) {
  if (Array.isArray(obj)) {
    return `[${obj.map(canonicalStringify)}]`;
  }
  if (!obj || typeof obj !== 'object') {
    return JSON.stringify(obj);
  }
  const keys = Object.keys(obj);

  // This is important for properly handling Date
  if (!keys.length) {
    return JSON.stringify(obj);
  }
  const sortedObj = keys.sort((a, b) => a.localeCompare(b)).map(k => `${k}: ${canonicalStringify(obj[k])}`);
  return `{${sortedObj}}`;
}

// Convert an object's values to md5 hash strings
function md5Values(obj) {
  return (0, _lodash.mapValues)(obj, md5Object);
}

// If something exists in actual, but is missing in expected, we don't
// care, as it could be a disabled plugin, etc, and keeping stale stuff
// around is better than migrating unecessesarily.
function findChangedProp(actual, expected) {
  return Object.keys(expected).find(k => actual[k] !== expected[k]);
}

/**
 * These mappings are required for any saved object index.
 *
 * @returns {IndexMapping}
 */
function getBaseMappings() {
  return {
    dynamic: 'strict',
    properties: {
      type: {
        type: 'keyword'
      },
      namespace: {
        type: 'keyword'
      },
      namespaces: {
        type: 'keyword'
      },
      originId: {
        type: 'keyword'
      },
      updated_at: {
        type: 'date'
      },
      created_at: {
        type: 'date'
      },
      references: {
        type: 'nested',
        properties: {
          name: {
            type: 'keyword'
          },
          type: {
            type: 'keyword'
          },
          id: {
            type: 'keyword'
          }
        }
      },
      coreMigrationVersion: {
        type: 'keyword'
      },
      typeMigrationVersion: {
        type: 'version'
      },
      managed: {
        type: 'boolean'
      }
    }
  };
}
function validateAndMerge(dest, source) {
  Object.keys(source).forEach(k => {
    if (k.startsWith('_')) {
      throw new Error(`Invalid mapping "${k}". Mappings cannot start with _.`);
    }
    if (dest.hasOwnProperty(k)) {
      throw new Error(`Cannot redefine core mapping "${k}".`);
    }
  });
  return Object.assign(dest, source);
}