"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.currentBackend = exports.LocalStorageAuthStore = exports.Backend = void 0;
exports.expandSearchEntries = expandSearchEntries;
exports.extractSearchFields = extractSearchFields;
exports.mergeExpandedEntries = mergeExpandedEntries;
exports.resolveBackend = resolveBackend;
exports.slugFromCustomPath = slugFromCustomPath;
var _set2 = _interopRequireDefault(require("lodash/set"));
var _get2 = _interopRequireDefault(require("lodash/get"));
var _sortBy2 = _interopRequireDefault(require("lodash/sortBy"));
var _trim2 = _interopRequireDefault(require("lodash/trim"));
var _uniq2 = _interopRequireDefault(require("lodash/uniq"));
var _isError2 = _interopRequireDefault(require("lodash/isError"));
var _flatten2 = _interopRequireDefault(require("lodash/flatten"));
var _attempt2 = _interopRequireDefault(require("lodash/attempt"));
var _immutable = require("immutable");
var fuzzy = _interopRequireWildcard(require("fuzzy"));
var _decapCmsLibUtil = require("decap-cms-lib-util");
var _path = require("path");
var _decapCmsLibWidgets = require("decap-cms-lib-widgets");
var _formats = require("./formats/formats");
var _config = require("./reducers/config");
var _entries = require("./reducers/entries");
var _integrations = require("./reducers/integrations");
var _collections = require("./reducers/collections");
var _Entry = require("./valueObjects/Entry");
var _urlHelper = require("./lib/urlHelper");
var _registry = require("./lib/registry");
var _formatters = require("./lib/formatters");
var _publishModes = require("./constants/publishModes");
var _collectionTypes = require("./constants/collectionTypes");
var _entryDraft = require("./reducers/entryDraft");
var _i18n = require("./lib/i18n");
const _excluded = ["field"];
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : String(i); }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
const {
  extractTemplateVars,
  dateParsers,
  expandPath
} = _decapCmsLibWidgets.stringTemplate;
function updateAssetProxies(assetProxies, config, collection, entryDraft, path) {
  assetProxies.map(asset => {
    // update media files path based on entry path
    const oldPath = asset.path;
    const newPath = (0, _entries.selectMediaFilePath)(config, collection, entryDraft.get('entry').set('path', path), oldPath, asset.field);
    asset.path = newPath;
  });
}
class LocalStorageAuthStore {
  constructor() {
    _defineProperty(this, "storageKey", 'decap-cms-user');
  }
  retrieve() {
    const data = window.localStorage.getItem(this.storageKey);
    return data && JSON.parse(data);
  }
  store(userData) {
    window.localStorage.setItem(this.storageKey, JSON.stringify(userData));
  }
  logout() {
    window.localStorage.removeItem(this.storageKey);
  }
}
exports.LocalStorageAuthStore = LocalStorageAuthStore;
function getEntryBackupKey(collectionName, slug) {
  const baseKey = 'backup';
  if (!collectionName) {
    return baseKey;
  }
  const suffix = slug ? `.${slug}` : '';
  return `${baseKey}.${collectionName}${suffix}`;
}
function getEntryField(field, entry) {
  const value = (0, _get2.default)(entry.data, field);
  if (value) {
    return String(value);
  } else {
    const firstFieldPart = field.split('.')[0];
    if (entry[firstFieldPart]) {
      // allows searching using entry.slug/entry.path etc.
      return entry[firstFieldPart];
    } else {
      return '';
    }
  }
}
function extractSearchFields(searchFields) {
  return entry => searchFields.reduce((acc, field) => {
    const value = getEntryField(field, entry);
    if (value) {
      return `${acc} ${value}`;
    } else {
      return acc;
    }
  }, '');
}
function expandSearchEntries(entries, searchFields) {
  // expand the entries for the purpose of the search
  const expandedEntries = entries.reduce((acc, e) => {
    const expandedFields = searchFields.reduce((acc, f) => {
      const fields = expandPath({
        data: e.data,
        path: f
      });
      acc.push(...fields);
      return acc;
    }, []);
    for (let i = 0; i < expandedFields.length; i++) {
      acc.push(_objectSpread(_objectSpread({}, e), {}, {
        field: expandedFields[i]
      }));
    }
    return acc;
  }, []);
  return expandedEntries;
}
function mergeExpandedEntries(entries) {
  // merge the search results by slug and only keep data that matched the search
  const fields = entries.map(f => f.field);
  const arrayPaths = {};
  const merged = entries.reduce((acc, e) => {
    if (!acc[e.slug]) {
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const {
          field
        } = e,
        rest = _objectWithoutProperties(e, _excluded);
      acc[e.slug] = rest;
      arrayPaths[e.slug] = (0, _immutable.Set)();
    }
    const nestedFields = e.field.split('.');
    let value = acc[e.slug].data;
    for (let i = 0; i < nestedFields.length; i++) {
      value = value[nestedFields[i]];
      if (Array.isArray(value)) {
        const path = nestedFields.slice(0, i + 1).join('.');
        arrayPaths[e.slug] = arrayPaths[e.slug].add(path);
      }
    }
    return acc;
  }, {});

  // this keeps the search score sorting order designated by the order in entries
  // and filters non matching items
  Object.keys(merged).forEach(slug => {
    const data = merged[slug].data;
    for (const path of arrayPaths[slug].toArray()) {
      const array = (0, _get2.default)(data, path);
      const filtered = array.filter((_, index) => {
        return fields.some(f => `${f}.`.startsWith(`${path}.${index}.`));
      });
      filtered.sort((a, b) => {
        const indexOfA = array.indexOf(a);
        const indexOfB = array.indexOf(b);
        const pathOfA = `${path}.${indexOfA}.`;
        const pathOfB = `${path}.${indexOfB}.`;
        const matchingFieldIndexA = fields.findIndex(f => `${f}.`.startsWith(pathOfA));
        const matchingFieldIndexB = fields.findIndex(f => `${f}.`.startsWith(pathOfB));
        return matchingFieldIndexA - matchingFieldIndexB;
      });
      (0, _set2.default)(data, path, filtered);
    }
  });
  return Object.values(merged);
}
function sortByScore(a, b) {
  if (a.score > b.score) return -1;
  if (a.score < b.score) return 1;
  return 0;
}
function slugFromCustomPath(collection, customPath) {
  const folderPath = collection.get('folder', '');
  const entryPath = customPath.toLowerCase().replace(folderPath.toLowerCase(), '');
  const slug = (0, _path.join)((0, _path.dirname)((0, _trim2.default)(entryPath, '/')), (0, _path.basename)(entryPath, (0, _path.extname)(customPath)));
  return slug;
}
function prepareMetaPath(path, collection) {
  if (!(0, _collections.selectHasMetaPath)(collection)) {
    return path;
  }
  const dir = (0, _path.dirname)(path);
  return dir.slice(collection.get('folder').length + 1) || '/';
}
function collectionDepth(collection) {
  var _collection$get;
  let depth;
  depth = ((_collection$get = collection.get('nested')) === null || _collection$get === void 0 ? void 0 : _collection$get.get('depth')) || (0, _decapCmsLibUtil.getPathDepth)(collection.get('path', ''));
  if ((0, _i18n.hasI18n)(collection)) {
    depth = (0, _i18n.getI18nFilesDepth)(collection, depth);
  }
  return depth;
}
function i18nRulestring(ruleString, {
  defaultLocale,
  structure
}) {
  if (structure === _i18n.I18N_STRUCTURE.MULTIPLE_FOLDERS) {
    return `${defaultLocale}\\/${ruleString}`;
  }
  if (structure === _i18n.I18N_STRUCTURE.MULTIPLE_FILES) {
    return `${ruleString}\\.${defaultLocale}\\..*`;
  }
  return ruleString;
}
function collectionRegex(collection) {
  let ruleString = '';
  if (collection.get('path')) {
    ruleString = `${collection.get('folder')}/${collection.get('path')}`.replace(/{{.*}}/gm, '(.*)');
  }
  if ((0, _i18n.hasI18n)(collection)) {
    ruleString = i18nRulestring(ruleString, (0, _i18n.getI18nInfo)(collection));
  }
  return ruleString ? new RegExp(ruleString) : undefined;
}
class Backend {
  constructor(implementation, {
    backendName,
    authStore,
    config
  }) {
    _defineProperty(this, "implementation", void 0);
    _defineProperty(this, "backendName", void 0);
    _defineProperty(this, "config", void 0);
    _defineProperty(this, "authStore", void 0);
    _defineProperty(this, "user", void 0);
    _defineProperty(this, "backupSync", void 0);
    _defineProperty(this, "updateUserCredentials", updatedCredentials => {
      const storedUser = this.authStore.retrieve();
      if (storedUser && storedUser.backendName === this.backendName) {
        this.user = _objectSpread(_objectSpread({}, storedUser), updatedCredentials);
        this.authStore.store(this.user);
        return this.user;
      }
    });
    _defineProperty(this, "getToken", () => this.implementation.getToken());
    // We can't reliably run this on exit, so we do cleanup on load.
    this.deleteAnonymousBackup();
    this.config = config;
    this.implementation = implementation.init(this.config, {
      useWorkflow: (0, _config.selectUseWorkflow)(this.config),
      updateUserCredentials: this.updateUserCredentials,
      initialWorkflowStatus: _publishModes.status.first()
    });
    this.backendName = backendName;
    this.authStore = authStore;
    if (this.implementation === null) {
      throw new Error('Cannot instantiate a Backend with no implementation');
    }
    this.backupSync = (0, _decapCmsLibUtil.asyncLock)();
  }
  async status() {
    const attempts = 3;
    let status = {
      auth: {
        status: true
      },
      api: {
        status: true,
        statusPage: ''
      }
    };
    for (let i = 1; i <= attempts; i++) {
      status = await this.implementation.status();
      // return on first success
      if (Object.values(status).every(s => s.status === true)) {
        return status;
      } else {
        await new Promise(resolve => setTimeout(resolve, i * 1000));
      }
    }
    return status;
  }
  currentUser() {
    if (this.user) {
      return this.user;
    }
    const stored = this.authStore.retrieve();
    if (stored && stored.backendName === this.backendName) {
      return Promise.resolve(this.implementation.restoreUser(stored)).then(user => {
        this.user = _objectSpread(_objectSpread({}, user), {}, {
          backendName: this.backendName
        });
        // return confirmed/rehydrated user object instead of stored
        this.authStore.store(this.user);
        return this.user;
      });
    }
    return Promise.resolve(null);
  }
  isGitBackend() {
    var _this$implementation$, _this$implementation;
    return ((_this$implementation$ = (_this$implementation = this.implementation).isGitBackend) === null || _this$implementation$ === void 0 ? void 0 : _this$implementation$.call(_this$implementation)) || false;
  }
  authComponent() {
    return this.implementation.authComponent();
  }
  authenticate(credentials) {
    return this.implementation.authenticate(credentials).then(user => {
      this.user = _objectSpread(_objectSpread({}, user), {}, {
        backendName: this.backendName
      });
      if (this.authStore) {
        this.authStore.store(this.user);
      }
      return this.user;
    });
  }
  async logout() {
    try {
      await this.implementation.logout();
    } catch (e) {
      console.warn('Error during logout', e.message);
    } finally {
      this.user = null;
      if (this.authStore) {
        this.authStore.logout();
      }
    }
  }
  async entryExist(collection, path, slug, useWorkflow) {
    const unpublishedEntry = useWorkflow && (await this.implementation.unpublishedEntry({
      collection: collection.get('name'),
      slug
    }).catch(error => {
      if (error.name === _decapCmsLibUtil.EDITORIAL_WORKFLOW_ERROR && error.notUnderEditorialWorkflow) {
        return Promise.resolve(false);
      }
      return Promise.reject(error);
    }));
    if (unpublishedEntry) return unpublishedEntry;
    const publishedEntry = await this.implementation.getEntry(path).then(({
      data
    }) => data).catch(() => {
      return Promise.resolve(false);
    });
    return publishedEntry;
  }
  async generateUniqueSlug(collection, entryData, config, usedSlugs, customPath) {
    const slugConfig = config.slug;
    let slug;
    if (customPath) {
      slug = slugFromCustomPath(collection, customPath);
    } else {
      slug = (0, _formatters.slugFormatter)(collection, entryData, slugConfig);
    }
    let i = 1;
    let uniqueSlug = slug;

    // Check for duplicate slug in loaded entities store first before repo
    while (usedSlugs.includes(uniqueSlug) || (await this.entryExist(collection, (0, _collections.selectEntryPath)(collection, uniqueSlug), uniqueSlug, (0, _config.selectUseWorkflow)(config)))) {
      uniqueSlug = `${slug}${(0, _urlHelper.sanitizeChar)(' ', slugConfig)}${i++}`;
    }
    return uniqueSlug;
  }
  processEntries(loadedEntries, collection) {
    const entries = loadedEntries.map(loadedEntry => (0, _Entry.createEntry)(collection.get('name'), (0, _collections.selectEntrySlug)(collection, loadedEntry.file.path), loadedEntry.file.path, {
      raw: loadedEntry.data || '',
      label: loadedEntry.file.label,
      author: loadedEntry.file.author,
      updatedOn: loadedEntry.file.updatedOn,
      meta: {
        path: prepareMetaPath(loadedEntry.file.path, collection)
      }
    }));
    const formattedEntries = entries.map(this.entryWithFormat(collection));
    // If this collection has a "filter" property, filter entries accordingly
    const collectionFilter = collection.get('filter');
    const filteredEntries = collectionFilter ? this.filterEntries({
      entries: formattedEntries
    }, collectionFilter) : formattedEntries;
    if ((0, _i18n.hasI18n)(collection)) {
      const extension = (0, _collections.selectFolderEntryExtension)(collection);
      const groupedEntries = (0, _i18n.groupEntries)(collection, extension, filteredEntries);
      return groupedEntries;
    }
    return filteredEntries;
  }
  async listEntries(collection) {
    var _cursor$meta;
    const extension = (0, _collections.selectFolderEntryExtension)(collection);
    let listMethod;
    const collectionType = collection.get('type');
    if (collectionType === _collectionTypes.FOLDER) {
      listMethod = () => {
        const depth = collectionDepth(collection);
        return this.implementation.entriesByFolder(collection.get('folder'), extension, depth);
      };
    } else if (collectionType === _collectionTypes.FILES) {
      const files = collection.get('files').map(collectionFile => ({
        path: collectionFile.get('file'),
        label: collectionFile.get('label')
      })).toArray();
      listMethod = () => this.implementation.entriesByFiles(files);
    } else {
      throw new Error(`Unknown collection type: ${collectionType}`);
    }
    const loadedEntries = await listMethod();
    /*
          Wrap cursors so we can tell which collection the cursor is
          from. This is done to prevent traverseCursor from requiring a
          `collection` argument.
        */
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    const cursor = _decapCmsLibUtil.Cursor.create(loadedEntries[_decapCmsLibUtil.CURSOR_COMPATIBILITY_SYMBOL]).wrapData({
      cursorType: 'collectionEntries',
      collection
    });
    return {
      entries: this.processEntries(loadedEntries, collection),
      pagination: (_cursor$meta = cursor.meta) === null || _cursor$meta === void 0 ? void 0 : _cursor$meta.get('page'),
      cursor
    };
  }

  // The same as listEntries, except that if a cursor with the "next"
  // action available is returned, it calls "next" on the cursor and
  // repeats the process. Once there is no available "next" action, it
  // returns all the collected entries. Used to retrieve all entries
  // for local searches and queries.
  async listAllEntries(collection) {
    if (collection.get('folder') && this.implementation.allEntriesByFolder) {
      const depth = collectionDepth(collection);
      const extension = (0, _collections.selectFolderEntryExtension)(collection);
      return this.implementation.allEntriesByFolder(collection.get('folder'), extension, depth, collectionRegex(collection)).then(entries => this.processEntries(entries, collection));
    }
    const response = await this.listEntries(collection);
    const {
      entries
    } = response;
    let {
      cursor
    } = response;
    while (cursor && cursor.actions.includes('next')) {
      const {
        entries: newEntries,
        cursor: newCursor
      } = await this.traverseCursor(cursor, 'next');
      entries.push(...newEntries);
      cursor = newCursor;
    }
    return entries;
  }
  async search(collections, searchTerm) {
    // Perform a local search by requesting all entries. For each
    // collection, load it, search, and call onCollectionResults with
    // its results.
    const errors = [];
    const collectionEntriesRequests = collections.map(async collection => {
      const summary = collection.get('summary', '');
      const summaryFields = extractTemplateVars(summary);

      // TODO: pass search fields in as an argument
      let searchFields = [];
      if (collection.get('type') === _collectionTypes.FILES) {
        var _collection$get2;
        (_collection$get2 = collection.get('files')) === null || _collection$get2 === void 0 ? void 0 : _collection$get2.forEach(f => {
          const topLevelFields = f.get('fields').map(f => f.get('name')).toArray();
          searchFields = [...searchFields, ...topLevelFields];
        });
      } else {
        searchFields = [(0, _collections.selectInferredField)(collection, 'title'), (0, _collections.selectInferredField)(collection, 'shortTitle'), (0, _collections.selectInferredField)(collection, 'author'), ...summaryFields.map(elem => {
          if (dateParsers[elem]) {
            return (0, _collections.selectInferredField)(collection, 'date');
          }
          return elem;
        })];
      }
      const filteredSearchFields = searchFields.filter(Boolean);
      const collectionEntries = await this.listAllEntries(collection);
      return fuzzy.filter(searchTerm, collectionEntries, {
        extract: extractSearchFields((0, _uniq2.default)(filteredSearchFields))
      });
    }).map(p => p.catch(err => {
      errors.push(err);
      return [];
    }));
    const entries = await Promise.all(collectionEntriesRequests).then(arrays => (0, _flatten2.default)(arrays));
    if (errors.length > 0) {
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      throw new Error({
        message: 'Errors occurred while searching entries locally!',
        errors
      });
    }
    const hits = entries.filter(({
      score
    }) => score > 5).sort(sortByScore).map(f => f.original);
    return {
      entries: hits
    };
  }
  async query(collection, searchFields, searchTerm, file, limit) {
    let entries = await this.listAllEntries(collection);
    if (file) {
      entries = entries.filter(e => e.slug === file);
    }
    const expandedEntries = expandSearchEntries(entries, searchFields);
    let hits = fuzzy.filter(searchTerm, expandedEntries, {
      extract: entry => {
        return getEntryField(entry.field, entry);
      }
    }).sort(sortByScore).map(f => f.original);
    if (limit !== undefined && limit > 0) {
      hits = hits.slice(0, limit);
    }
    const merged = mergeExpandedEntries(hits);
    return {
      query: searchTerm,
      hits: merged
    };
  }
  traverseCursor(cursor, action) {
    const [data, unwrappedCursor] = cursor.unwrapData();
    // TODO: stop assuming all cursors are for collections
    const collection = data.get('collection');
    return this.implementation.traverseCursor(unwrappedCursor, action).then(async ({
      entries,
      cursor: newCursor
    }) => ({
      entries: this.processEntries(entries, collection),
      cursor: _decapCmsLibUtil.Cursor.create(newCursor).wrapData({
        cursorType: 'collectionEntries',
        collection
      })
    }));
  }
  async getLocalDraftBackup(collection, slug) {
    const key = getEntryBackupKey(collection.get('name'), slug);
    const backup = await _decapCmsLibUtil.localForage.getItem(key);
    if (!backup || !backup.raw.trim()) {
      return {};
    }
    const {
      raw,
      path
    } = backup;
    let {
      mediaFiles = []
    } = backup;
    mediaFiles = mediaFiles.map(file => {
      // de-serialize the file object
      if (file.file) {
        return _objectSpread(_objectSpread({}, file), {}, {
          url: URL.createObjectURL(file.file)
        });
      }
      return file;
    });
    const label = (0, _collections.selectFileEntryLabel)(collection, slug);
    const formatRawData = raw => {
      return this.entryWithFormat(collection)((0, _Entry.createEntry)(collection.get('name'), slug, path, {
        raw,
        label,
        mediaFiles,
        meta: {
          path: prepareMetaPath(path, collection)
        }
      }));
    };
    const entry = formatRawData(raw);
    if ((0, _i18n.hasI18n)(collection) && backup.i18n) {
      const i18n = (0, _i18n.formatI18nBackup)(backup.i18n, formatRawData);
      entry.i18n = i18n;
    }
    return {
      entry
    };
  }
  async persistLocalDraftBackup(entry, collection) {
    try {
      await this.backupSync.acquire();
      const key = getEntryBackupKey(collection.get('name'), entry.get('slug'));
      const raw = this.entryToRaw(collection, entry);
      if (!raw.trim()) {
        return;
      }
      const mediaFiles = await Promise.all(entry.get('mediaFiles').toJS().map(async file => {
        var _file$url;
        // make sure to serialize the file
        if ((_file$url = file.url) !== null && _file$url !== void 0 && _file$url.startsWith('blob:')) {
          const blob = await fetch(file.url).then(res => res.blob());
          return _objectSpread(_objectSpread({}, file), {}, {
            file: (0, _decapCmsLibUtil.blobToFileObj)(file.name, blob)
          });
        }
        return file;
      }));
      let i18n;
      if ((0, _i18n.hasI18n)(collection)) {
        i18n = (0, _i18n.getI18nBackup)(collection, entry, entry => this.entryToRaw(collection, entry));
      }
      await _decapCmsLibUtil.localForage.setItem(key, _objectSpread({
        raw,
        path: entry.get('path'),
        mediaFiles
      }, i18n && {
        i18n
      }));
      const result = await _decapCmsLibUtil.localForage.setItem(getEntryBackupKey(), raw);
      return result;
    } catch (e) {
      console.warn('persistLocalDraftBackup', e);
    } finally {
      this.backupSync.release();
    }
  }
  async deleteLocalDraftBackup(collection, slug) {
    try {
      await this.backupSync.acquire();
      await _decapCmsLibUtil.localForage.removeItem(getEntryBackupKey(collection.get('name'), slug));
      // delete new entry backup if not deleted
      slug && (await _decapCmsLibUtil.localForage.removeItem(getEntryBackupKey(collection.get('name'))));
      const result = await this.deleteAnonymousBackup();
      return result;
    } catch (e) {
      console.warn('deleteLocalDraftBackup', e);
    } finally {
      this.backupSync.release();
    }
  }

  // Unnamed backup for use in the global error boundary, should always be
  // deleted on cms load.
  deleteAnonymousBackup() {
    return _decapCmsLibUtil.localForage.removeItem(getEntryBackupKey());
  }
  async getEntry(state, collection, slug) {
    const path = (0, _collections.selectEntryPath)(collection, slug);
    const label = (0, _collections.selectFileEntryLabel)(collection, slug);
    const extension = (0, _collections.selectFolderEntryExtension)(collection);
    const getEntryValue = async path => {
      const loadedEntry = await this.implementation.getEntry(path);
      let entry = (0, _Entry.createEntry)(collection.get('name'), slug, loadedEntry.file.path, {
        raw: loadedEntry.data,
        label,
        mediaFiles: [],
        meta: {
          path: prepareMetaPath(loadedEntry.file.path, collection)
        }
      });
      entry = this.entryWithFormat(collection)(entry);
      entry = await this.processEntry(state, collection, entry);
      return entry;
    };
    let entryValue;
    if ((0, _i18n.hasI18n)(collection)) {
      entryValue = await (0, _i18n.getI18nEntry)(collection, extension, path, slug, getEntryValue);
    } else {
      entryValue = await getEntryValue(path);
    }
    return entryValue;
  }
  getMedia() {
    return this.implementation.getMedia();
  }
  getMediaFile(path) {
    return this.implementation.getMediaFile(path);
  }
  getMediaDisplayURL(displayURL) {
    if (this.implementation.getMediaDisplayURL) {
      return this.implementation.getMediaDisplayURL(displayURL);
    }
    const err = new Error('getMediaDisplayURL is not implemented by the current backend, but the backend returned a displayURL which was not a string!');
    err.displayURL = displayURL;
    return Promise.reject(err);
  }
  entryWithFormat(collection) {
    return entry => {
      const format = (0, _formats.resolveFormat)(collection, entry);
      if (entry && entry.raw !== undefined) {
        const data = format && (0, _attempt2.default)(format.fromFile.bind(format, entry.raw)) || {};
        if ((0, _isError2.default)(data)) console.error(data);
        return Object.assign(entry, {
          data: (0, _isError2.default)(data) ? {} : data
        });
      }
      return format.fromFile(entry);
    };
  }
  async processUnpublishedEntry(collection, entryData, withMediaFiles) {
    const {
      slug
    } = entryData;
    let extension;
    if (collection.get('type') === _collectionTypes.FILES) {
      const file = collection.get('files').find(f => (f === null || f === void 0 ? void 0 : f.get('name')) === slug);
      extension = (0, _path.extname)(file.get('file'));
    } else {
      extension = (0, _collections.selectFolderEntryExtension)(collection);
    }
    const mediaFiles = [];
    if (withMediaFiles) {
      const nonDataFiles = entryData.diffs.filter(d => !d.path.endsWith(extension));
      const files = await Promise.all(nonDataFiles.map(f => this.implementation.unpublishedEntryMediaFile(collection.get('name'), slug, f.path, f.id)));
      mediaFiles.push(...files.map(f => _objectSpread(_objectSpread({}, f), {}, {
        draft: true
      })));
    }
    const dataFiles = (0, _sortBy2.default)(entryData.diffs.filter(d => d.path.endsWith(extension)), f => f.path.length);
    const formatData = (data, path, newFile) => {
      const entry = (0, _Entry.createEntry)(collection.get('name'), slug, path, {
        raw: data,
        isModification: !newFile,
        label: collection && (0, _collections.selectFileEntryLabel)(collection, slug),
        mediaFiles,
        updatedOn: entryData.updatedAt,
        author: entryData.pullRequestAuthor,
        status: entryData.status,
        meta: {
          path: prepareMetaPath(path, collection)
        }
      });
      const entryWithFormat = this.entryWithFormat(collection)(entry);
      return entryWithFormat;
    };
    const readAndFormatDataFile = async dataFile => {
      const data = await this.implementation.unpublishedEntryDataFile(collection.get('name'), entryData.slug, dataFile.path, dataFile.id);
      const entryWithFormat = formatData(data, dataFile.path, dataFile.newFile);
      return entryWithFormat;
    };

    // if the unpublished entry has no diffs, return the original
    if (dataFiles.length <= 0) {
      const loadedEntry = await this.implementation.getEntry((0, _collections.selectEntryPath)(collection, slug));
      return formatData(loadedEntry.data, loadedEntry.file.path, false);
    } else if ((0, _i18n.hasI18n)(collection)) {
      // we need to read all locales files and not just the changes
      const path = (0, _collections.selectEntryPath)(collection, slug);
      const i18nFiles = (0, _i18n.getI18nDataFiles)(collection, extension, path, slug, dataFiles);
      let entries = await Promise.all(i18nFiles.map(dataFile => readAndFormatDataFile(dataFile).catch(() => null)));
      entries = entries.filter(Boolean);
      const grouped = await (0, _i18n.groupEntries)(collection, extension, entries);
      return grouped[0];
    } else {
      const entryWithFormat = await readAndFormatDataFile(dataFiles[0]);
      return entryWithFormat;
    }
  }
  async unpublishedEntries(collections) {
    const ids = await this.implementation.unpublishedEntries();
    const entries = (await Promise.all(ids.map(async id => {
      const entryData = await this.implementation.unpublishedEntry({
        id
      });
      const collectionName = entryData.collection;
      const collection = collections.find(c => c.get('name') === collectionName);
      if (!collection) {
        console.warn(`Missing collection '${collectionName}' for unpublished entry '${id}'`);
        return null;
      }
      const entry = await this.processUnpublishedEntry(collection, entryData, false);
      return entry;
    }))).filter(Boolean);
    return {
      pagination: 0,
      entries
    };
  }
  async processEntry(state, collection, entry) {
    const integration = (0, _integrations.selectIntegration)(state.integrations, null, 'assetStore');
    const mediaFolders = (0, _collections.selectMediaFolders)(state.config, collection, (0, _immutable.fromJS)(entry));
    if (mediaFolders.length > 0 && !integration) {
      const files = await Promise.all(mediaFolders.map(folder => this.implementation.getMedia(folder)));
      entry.mediaFiles = entry.mediaFiles.concat(...files);
    } else {
      entry.mediaFiles = entry.mediaFiles.concat(state.mediaLibrary.get('files') || []);
    }
    return entry;
  }
  async unpublishedEntry(state, collection, slug) {
    const entryData = await this.implementation.unpublishedEntry({
      collection: collection.get('name'),
      slug
    });
    let entry = await this.processUnpublishedEntry(collection, entryData, true);
    entry = await this.processEntry(state, collection, entry);
    return entry;
  }

  /**
   * Creates a URL using `site_url` from the config and `preview_path` from the
   * entry's collection. Does not currently make a request through the backend,
   * but likely will in the future.
   */
  getDeploy(collection, slug, entry) {
    /**
     * If `site_url` is undefined or `show_preview_links` in the config is set to false, do nothing.
     */

    const baseUrl = this.config.site_url;
    if (!baseUrl || this.config.show_preview_links === false) {
      return;
    }
    return {
      url: (0, _formatters.previewUrlFormatter)(baseUrl, collection, slug, entry, this.config.slug),
      status: 'SUCCESS'
    };
  }

  /**
   * Requests a base URL from the backend for previewing a specific entry.
   * Supports polling via `maxAttempts` and `interval` options, as there is
   * often a delay before a preview URL is available.
   */
  async getDeployPreview(collection, slug, entry, {
    maxAttempts = 1,
    interval = 5000
  } = {}) {
    /**
     * If the registered backend does not provide a `getDeployPreview` method, or
     * `show_preview_links` in the config is set to false, do nothing.
     */
    if (!this.implementation.getDeployPreview || this.config.show_preview_links === false) {
      return;
    }

    /**
     * Poll for the deploy preview URL (defaults to 1 attempt, so no polling by
     * default).
     */
    let deployPreview,
      count = 0;
    while (!deployPreview && count < maxAttempts) {
      count++;
      deployPreview = await this.implementation.getDeployPreview(collection.get('name'), slug);
      if (!deployPreview) {
        await new Promise(resolve => setTimeout(() => resolve(undefined), interval));
      }
    }

    /**
     * If there's no deploy preview, do nothing.
     */
    if (!deployPreview) {
      return;
    }
    return {
      /**
       * Create a URL using the collection `preview_path`, if provided.
       */
      url: (0, _formatters.previewUrlFormatter)(deployPreview.url, collection, slug, entry, this.config.slug),
      /**
       * Always capitalize the status for consistency.
       */
      status: deployPreview.status ? deployPreview.status.toUpperCase() : ''
    };
  }
  async persistEntry({
    config,
    collection,
    entryDraft: draft,
    assetProxies,
    usedSlugs,
    unpublished = false,
    status
  }) {
    const updatedEntity = await this.invokePreSaveEvent(draft.get('entry'));
    let entryDraft;
    if (updatedEntity.get('data') === undefined) {
      entryDraft = updatedEntity && draft.setIn(['entry', 'data'], updatedEntity) || draft;
    } else {
      entryDraft = updatedEntity && draft.setIn(['entry'], updatedEntity) || draft;
    }
    const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false;
    const useWorkflow = (0, _config.selectUseWorkflow)(config);
    const customPath = (0, _entryDraft.selectCustomPath)(collection, entryDraft);
    let dataFile;
    if (newEntry) {
      if (!(0, _collections.selectAllowNewEntries)(collection)) {
        throw new Error('Not allowed to create new entries in this collection');
      }
      const slug = await this.generateUniqueSlug(collection, entryDraft.getIn(['entry', 'data']), config, usedSlugs, customPath);
      const path = customPath || (0, _collections.selectEntryPath)(collection, slug);
      dataFile = {
        path,
        slug,
        raw: this.entryToRaw(collection, entryDraft.get('entry'))
      };
      updateAssetProxies(assetProxies, config, collection, entryDraft, path);
    } else {
      const slug = entryDraft.getIn(['entry', 'slug']);
      dataFile = {
        path: entryDraft.getIn(['entry', 'path']),
        // for workflow entries we refresh the slug on publish
        slug: customPath && !useWorkflow ? slugFromCustomPath(collection, customPath) : slug,
        raw: this.entryToRaw(collection, entryDraft.get('entry')),
        newPath: customPath
      };
    }
    const {
      slug,
      path,
      newPath
    } = dataFile;
    let dataFiles = [dataFile];
    if ((0, _i18n.hasI18n)(collection)) {
      const extension = (0, _collections.selectFolderEntryExtension)(collection);
      dataFiles = (0, _i18n.getI18nFiles)(collection, extension, entryDraft.get('entry'), draftData => this.entryToRaw(collection, draftData), path, slug, newPath);
    }
    const user = await this.currentUser();
    const commitMessage = (0, _formatters.commitMessageFormatter)(newEntry ? 'create' : 'update', config, {
      collection,
      slug,
      path,
      authorLogin: user.login,
      authorName: user.name
    }, user.useOpenAuthoring);
    const collectionName = collection.get('name');
    const updatedOptions = {
      unpublished,
      status
    };
    const opts = _objectSpread({
      newEntry,
      commitMessage,
      collectionName,
      useWorkflow
    }, updatedOptions);
    if (!useWorkflow) {
      await this.invokePrePublishEvent(entryDraft.get('entry'));
    }
    await this.implementation.persistEntry({
      dataFiles,
      assets: assetProxies
    }, opts);
    await this.invokePostSaveEvent(entryDraft.get('entry'));
    if (!useWorkflow) {
      await this.invokePostPublishEvent(entryDraft.get('entry'));
    }
    return slug;
  }
  async invokeEventWithEntry(event, entry) {
    const {
      login,
      name
    } = await this.currentUser();
    return await (0, _registry.invokeEvent)({
      name: event,
      data: {
        entry,
        author: {
          login,
          name
        }
      }
    });
  }
  async invokePrePublishEvent(entry) {
    await this.invokeEventWithEntry('prePublish', entry);
  }
  async invokePostPublishEvent(entry) {
    await this.invokeEventWithEntry('postPublish', entry);
  }
  async invokePreUnpublishEvent(entry) {
    await this.invokeEventWithEntry('preUnpublish', entry);
  }
  async invokePostUnpublishEvent(entry) {
    await this.invokeEventWithEntry('postUnpublish', entry);
  }
  async invokePreSaveEvent(entry) {
    return await this.invokeEventWithEntry('preSave', entry);
  }
  async invokePostSaveEvent(entry) {
    await this.invokeEventWithEntry('postSave', entry);
  }
  async persistMedia(config, file) {
    const user = await this.currentUser();
    const options = {
      commitMessage: (0, _formatters.commitMessageFormatter)('uploadMedia', config, {
        path: file.path,
        authorLogin: user.login,
        authorName: user.name
      }, user.useOpenAuthoring)
    };
    return this.implementation.persistMedia(file, options);
  }
  async deleteEntry(state, collection, slug) {
    const config = state.config;
    const path = (0, _collections.selectEntryPath)(collection, slug);
    const extension = (0, _collections.selectFolderEntryExtension)(collection);
    if (!(0, _collections.selectAllowDeletion)(collection)) {
      throw new Error('Not allowed to delete entries in this collection');
    }
    const user = await this.currentUser();
    const commitMessage = (0, _formatters.commitMessageFormatter)('delete', config, {
      collection,
      slug,
      path,
      authorLogin: user.login,
      authorName: user.name
    }, user.useOpenAuthoring);
    const entry = (0, _entries.selectEntry)(state.entries, collection.get('name'), slug);
    await this.invokePreUnpublishEvent(entry);
    let paths = [path];
    if ((0, _i18n.hasI18n)(collection)) {
      paths = (0, _i18n.getFilePaths)(collection, extension, path, slug);
    }
    await this.implementation.deleteFiles(paths, commitMessage);
    await this.invokePostUnpublishEvent(entry);
  }
  async deleteMedia(config, path) {
    const user = await this.currentUser();
    const commitMessage = (0, _formatters.commitMessageFormatter)('deleteMedia', config, {
      path,
      authorLogin: user.login,
      authorName: user.name
    }, user.useOpenAuthoring);
    return this.implementation.deleteFiles([path], commitMessage);
  }
  persistUnpublishedEntry(args) {
    return this.persistEntry(_objectSpread(_objectSpread({}, args), {}, {
      unpublished: true
    }));
  }
  updateUnpublishedEntryStatus(collection, slug, newStatus) {
    return this.implementation.updateUnpublishedEntryStatus(collection, slug, newStatus);
  }
  async publishUnpublishedEntry(entry) {
    const collection = entry.get('collection');
    const slug = entry.get('slug');
    await this.invokePrePublishEvent(entry);
    await this.implementation.publishUnpublishedEntry(collection, slug);
    await this.invokePostPublishEvent(entry);
  }
  deleteUnpublishedEntry(collection, slug) {
    return this.implementation.deleteUnpublishedEntry(collection, slug);
  }
  entryToRaw(collection, entry) {
    const format = (0, _formats.resolveFormat)(collection, entry.toJS());
    const fieldsOrder = this.fieldsOrder(collection, entry);
    const fieldsComments = (0, _collections.selectFieldsComments)(collection, entry);
    let content = format.toFile(entry.get('data').toJS(), fieldsOrder, fieldsComments);
    if (content.slice(-1) != '\n') {
      // add the EOL if it does not exist.
      content += '\n';
    }
    return content;
  }
  fieldsOrder(collection, entry) {
    const fields = collection.get('fields');
    if (fields) {
      return collection.get('fields').map(f => f.get('name')).toArray();
    }
    const files = collection.get('files');
    const file = (files || (0, _immutable.List)()).filter(f => f.get('name') === entry.get('slug')).get(0);
    if (file == null) {
      throw new Error(`No file found for ${entry.get('slug')} in ${collection.get('name')}`);
    }
    return file.get('fields').map(f => f.get('name')).toArray();
  }
  filterEntries(collection, filterRule) {
    return collection.entries.filter(entry => {
      const fieldValue = entry.data[filterRule.get('field')];
      if (Array.isArray(fieldValue)) {
        return fieldValue.includes(filterRule.get('value'));
      }
      return fieldValue === filterRule.get('value');
    });
  }
}
exports.Backend = Backend;
function resolveBackend(config) {
  if (!config.backend.name) {
    throw new Error('No backend defined in configuration');
  }
  const {
    name
  } = config.backend;
  const authStore = new LocalStorageAuthStore();
  const backend = (0, _registry.getBackend)(name);
  if (!backend) {
    throw new Error(`Backend not found: ${name}`);
  } else {
    return new Backend(backend, {
      backendName: name,
      authStore,
      config
    });
  }
}
const currentBackend = exports.currentBackend = function () {
  let backend;
  return config => {
    if (backend) {
      return backend;
    }
    return backend = resolveBackend(config);
  };
}();