"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.resolveBackend = resolveBackend;
exports.currentBackend = exports.Backend = exports.LocalStorageAuthStore = void 0;

var _findLastIndex2 = _interopRequireDefault(require("lodash/findLastIndex"));

var _drop2 = _interopRequireDefault(require("lodash/drop"));

var _uniq2 = _interopRequireDefault(require("lodash/uniq"));

var _partialRight2 = _interopRequireDefault(require("lodash/partialRight"));

var _flow2 = _interopRequireDefault(require("lodash/flow"));

var _trimEnd2 = _interopRequireDefault(require("lodash/trimEnd"));

var _trimStart2 = _interopRequireDefault(require("lodash/trimStart"));

var _isError2 = _interopRequireDefault(require("lodash/isError"));

var _flatten2 = _interopRequireDefault(require("lodash/flatten"));

var _attempt2 = _interopRequireDefault(require("lodash/attempt"));

var _immutable = require("immutable");

var _commonTags = require("common-tags");

var _fuzzy = _interopRequireDefault(require("fuzzy"));

var _formats = require("./formats/formats");

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 _netlifyCmsLibUtil = require("netlify-cms-lib-util");

var _publishModes = require("./constants/publishModes");

var _stringTemplate = require("./lib/stringTemplate");

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }

function _templateObject() {
  const data = _taggedTemplateLiteral(["\n        Collection \"", "\" configuration error:\n          `preview_path_date_field` must be a field with a valid date. Ignoring `preview_path`.\n      "], ["\n        Collection \"", "\" configuration error:\n          \\`preview_path_date_field\\` must be a field with a valid date. Ignoring \\`preview_path\\`.\n      "]);

  _templateObject = function _templateObject() {
    return data;
  };

  return data;
}

function _taggedTemplateLiteral(strings, raw) { if (!raw) { raw = strings.slice(0); } return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

class LocalStorageAuthStore {
  constructor() {
    _defineProperty(this, "storageKey", 'netlify-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 prepareSlug(slug) {
  return slug.trim() // Convert slug to lower-case
  .toLocaleLowerCase() // Remove single quotes.
  .replace(/[']/g, '') // Replace periods with dashes.
  .replace(/[.]/g, '-');
}

function getEntryBackupKey(collectionName, slug) {
  const baseKey = 'backup';

  if (!collectionName) {
    return baseKey;
  }

  const suffix = slug ? ".".concat(slug) : '';
  return "".concat(baseKey, ".").concat(collectionName).concat(suffix);
}

function getLabelForFileCollectionEntry(collection, path) {
  const files = collection.get('files');
  return files && files.find(f => f.get('file') === path).get('label');
}

function slugFormatter(collection, entryData, slugConfig) {
  const template = collection.get('slug') || '{{slug}}';
  const identifier = entryData.get((0, _collections.selectIdentifier)(collection));

  if (!identifier) {
    throw new Error('Collection must have a field name that is a valid entry identifier, or must have `identifier_field` set');
  } // Pass entire slug through `prepareSlug` and `sanitizeSlug`.
  // TODO: only pass slug replacements through sanitizers, static portions of
  // the slug template should not be sanitized. (breaking change)


  const processSlug = (0, _flow2.default)([_stringTemplate.compileStringTemplate, prepareSlug, (0, _partialRight2.default)(_urlHelper.sanitizeSlug, slugConfig)]);
  return processSlug(template, new Date(), identifier, entryData);
}

const commitMessageTemplates = (0, _immutable.Map)({
  create: 'Create {{collection}} “{{slug}}”',
  update: 'Update {{collection}} “{{slug}}”',
  delete: 'Delete {{collection}} “{{slug}}”',
  revert: 'Revert {{collection}} “{{slug}}”',
  uploadMedia: 'Upload “{{path}}”',
  deleteMedia: 'Delete “{{path}}”'
});
const NETLIFY_ACTION = 'Netlify_Action';
const REVERT_REFERENCE = 'Revert_Reference';
const META_SEPARATOR = ';';
const ACTION_SEPARATOR = ':';
const DELETION_META_TYPE = 'DELETE';
const CREATION_META_TYPE = 'CREATE';
const UPDATE_META_TYPE = 'UPDATE';
const REVERT_META_TYPE = 'REVERT';

const commitMetaParser = commitMessage => {
  const lines = commitMessage.split(/\r\n|\r|\n/);
  const netlifyMetaLine = lines[lines.length - 1];
  const metaFields = netlifyMetaLine.split(META_SEPARATOR);
  return metaFields.map(metaField => {
    const [key, value] = metaField.split(ACTION_SEPARATOR);
    return {
      key,
      value
    };
  });
};

const commitMetaTypeParser = commitMessage => {
  try {
    const meta = commitMetaParser(commitMessage);
    const {
      value
    } = meta.find((_ref) => {
      let {
        key
      } = _ref;
      return key === NETLIFY_ACTION;
    });

    if (!Object.values(metaTypes).includes(value)) {
      return null;
    }

    return value;
  } catch (err) {
    return null;
  }
};

const extractReversionRef = message => {
  try {
    const meta = commitMetaParser(message);
    const reversion = meta.find((_ref2) => {
      let {
        key
      } = _ref2;
      return key === REVERT_REFERENCE;
    });

    if (!reversion) {
      return null;
    }

    const {
      value
    } = reversion;
    return value;
  } catch (err) {
    return null;
  }
};

const dropUnrelatedCommits = commits => {
  const reversionRefs = commits.map((_ref3) => {
    let {
      message
    } = _ref3;
    return extractReversionRef(message);
  }).filter(ref => !!ref);
  const controlledCommits = commits.filter((_ref4) => {
    let {
      ref
    } = _ref4;
    return !reversionRefs.includes(ref);
  });
  const deletionIndex = (0, _findLastIndex2.default)(controlledCommits, (_ref5) => {
    let {
      message
    } = _ref5;
    const metaType = commitMetaTypeParser(message);
    return metaType === DELETION_META_TYPE;
  });

  if (deletionIndex === -1) {
    return controlledCommits;
  }

  const stuff = (0, _drop2.default)(controlledCommits, deletionIndex + 1);
  return stuff;
};

const metaTypes = {
  update: UPDATE_META_TYPE,
  create: CREATION_META_TYPE,
  delete: DELETION_META_TYPE,
  revert: REVERT_META_TYPE
}; // Implying the meta action line to be the last line in the commit!!!

const commitMetaFormatter = (type, message, _ref6) => {
  let {
    ref,
    revert
  } = _ref6;
  const metaType = metaTypes[type];

  if (!metaType) {
    return message;
  }

  const referencedCommit = revert && ref ? "".concat(META_SEPARATOR).concat(REVERT_REFERENCE).concat(ACTION_SEPARATOR).concat(ref) : '';
  const plainMeta = "".concat(message, "\n").concat(NETLIFY_ACTION).concat(ACTION_SEPARATOR).concat(metaType);
  return "".concat(plainMeta).concat(referencedCommit);
};

const commitMessageFormatter = (type, config, _ref7) => {
  let {
    slug,
    path,
    collection,
    ref,
    revert
  } = _ref7;
  const templates = commitMessageTemplates.merge(config.getIn(['backend', 'commit_messages'], (0, _immutable.Map)()));
  const messageTemplate = templates.get(type);
  const interpolated = messageTemplate.replace(/\{\{([^}]+)\}\}/g, (_, variable) => {
    switch (variable) {
      case 'slug':
        return slug;

      case 'path':
        return path;

      case 'collection':
        return collection.get('label_singular') || collection.get('label');

      default:
        console.warn("Ignoring unknown variable \u201C".concat(variable, "\u201D in commit message template."));
        return '';
    }
  });
  const withMeta = commitMetaFormatter(type, interpolated, {
    ref,
    revert
  });
  return withMeta;
};

const extractSearchFields = searchFields => entry => searchFields.reduce((acc, field) => {
  let nestedFields = field.split('.');
  let f = entry.data;

  for (let i = 0; i < nestedFields.length; i++) {
    f = f[nestedFields[i]];
    if (!f) break;
  }

  return f ? "".concat(acc, " ").concat(f) : acc;
}, '');

const sortByScore = (a, b) => {
  if (a.score > b.score) return -1;
  if (a.score < b.score) return 1;
  return 0;
};

function createPreviewUrl(baseUrl, collection, slug, slugConfig, entry) {
  /**
   * Preview URL can't be created without `baseUrl`. This makes preview URLs
   * optional for backends that don't support them.
   */
  if (!baseUrl) {
    return;
  }
  /**
   * Without a `previewPath` for the collection (via config), the preview URL
   * will be the URL provided by the backend.
   */


  if (!collection.get('preview_path')) {
    return baseUrl;
  }
  /**
   * If a `previewPath` is provided for the collection, use it to construct the
   * URL path.
   */


  const basePath = (0, _trimEnd2.default)(baseUrl, '/');
  const pathTemplate = collection.get('preview_path');
  const fields = entry.get('data');
  const date = (0, _stringTemplate.parseDateFromEntry)(entry, collection, collection.get('preview_path_date_field')); // Prepare and sanitize slug variables only, leave the rest of the
  // `preview_path` template as is.

  const processSegment = (0, _flow2.default)([value => String(value), prepareSlug, (0, _partialRight2.default)(_urlHelper.sanitizeSlug, slugConfig)]);
  let compiledPath;

  try {
    compiledPath = (0, _stringTemplate.compileStringTemplate)(pathTemplate, date, slug, fields, processSegment);
  } catch (err) {
    // Print an error and ignore `preview_path` if both:
    //   1. Date is invalid (according to Moment), and
    //   2. A date expression (eg. `{{year}}`) is used in `preview_path`
    if (err.name === _stringTemplate.SLUG_MISSING_REQUIRED_DATE) {
      console.error((0, _commonTags.stripIndent)(_templateObject(), collection.get('name')));
      return basePath;
    }

    throw err;
  }

  const previewPath = (0, _trimStart2.default)(compiledPath, ' /');
  return "".concat(basePath, "/").concat(previewPath);
}

class Backend {
  constructor(implementation) {
    let {
      backendName,
      authStore = null,
      config
    } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

    _defineProperty(this, "updateUserCredentials", updatedCredentials => {
      const storedUser = this.authStore && this.authStore.retrieve();

      if (storedUser && storedUser.backendName === this.backendName) {
        this.user = _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(config, {
      useWorkflow: config.getIn(['publish_mode']) === _publishModes.EDITORIAL_WORKFLOW,
      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');
    }
  }

  currentUser() {
    if (this.user) {
      return this.user;
    }

    const stored = this.authStore && this.authStore.retrieve();

    if (stored && stored.backendName === this.backendName) {
      return Promise.resolve(this.implementation.restoreUser(stored)).then(user => {
        this.user = _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);
  }

  authComponent() {
    return this.implementation.authComponent();
  }

  authenticate(credentials) {
    return this.implementation.authenticate(credentials).then(user => {
      this.user = _objectSpread({}, user, {
        backendName: this.backendName
      });

      if (this.authStore) {
        this.authStore.store(this.user);
      }

      return this.user;
    });
  }

  logout() {
    return Promise.resolve(this.implementation.logout()).then(() => {
      this.user = null;

      if (this.authStore) {
        this.authStore.logout();
      }
    });
  }

  async entryExist(collection, path, slug) {
    const unpublishedEntry = this.implementation.unpublishedEntry && (await this.implementation.unpublishedEntry(collection, slug).catch(error => {
      if (error instanceof _netlifyCmsLibUtil.EditorialWorkflowError && error.notUnderEditorialWorkflow) {
        return Promise.resolve(false);
      }

      return Promise.reject(error);
    }));
    if (unpublishedEntry) return unpublishedEntry;
    const publishedEntry = await this.implementation.getEntry(collection, slug, path).then((_ref8) => {
      let {
        data
      } = _ref8;
      return data;
    }).catch(() => {
      return Promise.resolve(false);
    });
    return publishedEntry;
  }

  async generateUniqueSlug(collection, entryData, slugConfig, usedSlugs) {
    const slug = slugFormatter(collection, entryData, slugConfig);
    const sanitizeEntrySlug = (0, _partialRight2.default)(_urlHelper.sanitizeSlug, slugConfig);
    let i = 1;
    let sanitizedSlug = slug;
    let uniqueSlug = sanitizedSlug; // 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))) {
      uniqueSlug = sanitizeEntrySlug("".concat(sanitizedSlug, " ").concat(i++));
    }

    return uniqueSlug;
  }

  processEntries(loadedEntries, collection) {
    const collectionFilter = collection.get('filter');
    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,
      ref: loadedEntry.ref
    }));
    const formattedEntries = entries.map(this.entryWithFormat(collection)); // If this collection has a "filter" property, filter entries accordingly

    const filteredEntries = collectionFilter ? this.filterEntries({
      entries: formattedEntries
    }, collectionFilter) : formattedEntries;
    return filteredEntries;
  }

  listEntries(collection) {
    const listMethod = this.implementation[(0, _collections.selectListMethod)(collection)];
    const extension = (0, _collections.selectFolderEntryExtension)(collection);
    return listMethod.call(this.implementation, collection, extension).then(loadedEntries => ({
      entries: this.processEntries(loadedEntries, collection),

      /*
          Wrap cursors so we can tell which collection the cursor is
          from. This is done to prevent traverseCursor from requiring a
          `collection` argument.
        */
      cursor: _netlifyCmsLibUtil.Cursor.create(loadedEntries[_netlifyCmsLibUtil.CURSOR_COMPATIBILITY_SYMBOL]).wrapData({
        cursorType: 'collectionEntries',
        collection
      })
    }));
  } // 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 extension = (0, _collections.selectFolderEntryExtension)(collection);
      return this.implementation.allEntriesByFolder(collection, extension).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 = (0, _stringTemplate.extractTemplateVars)(summary); // TODO: pass search fields in as an argument

      const searchFields = [(0, _collections.selectInferedField)(collection, 'title'), (0, _collections.selectInferedField)(collection, 'shortTitle'), (0, _collections.selectInferedField)(collection, 'author'), ...summaryFields.map(elem => {
        if (_stringTemplate.dateParsers[elem]) {
          return (0, _collections.selectInferedField)(collection, 'date');
        }

        return elem;
      })].filter(Boolean);
      const collectionEntries = await this.listAllEntries(collection);
      return _fuzzy.default.filter(searchTerm, collectionEntries, {
        extract: extractSearchFields((0, _uniq2.default)(searchFields))
      });
    }).map(p => p.catch(err => errors.push(err) && []));
    const entries = await Promise.all(collectionEntriesRequests).then(arrs => (0, _flatten2.default)(arrs));

    if (errors.length > 0) {
      throw new Error({
        message: 'Errors ocurred while searching entries locally!',
        errors
      });
    }

    const hits = entries.filter((_ref9) => {
      let {
        score
      } = _ref9;
      return score > 5;
    }).sort(sortByScore).map(f => f.original);
    return {
      entries: hits
    };
  }

  async query(collection, searchFields, searchTerm) {
    const entries = await this.listAllEntries(collection);

    const hits = _fuzzy.default.filter(searchTerm, entries, {
      extract: extractSearchFields(searchFields)
    }).sort(sortByScore).map(f => f.original);

    return {
      query: searchTerm,
      hits
    };
  }

  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 (_ref10) => {
      let {
        entries,
        cursor: newCursor
      } = _ref10;
      return {
        entries: this.processEntries(entries, collection),
        cursor: _netlifyCmsLibUtil.Cursor.create(newCursor).wrapData({
          cursorType: 'collectionEntries',
          collection
        })
      };
    });
  }

  async getLocalDraftBackup(collection, slug) {
    const key = getEntryBackupKey(collection.get('name'), slug);
    const backup = await _netlifyCmsLibUtil.localForage.getItem(key);

    if (!backup || !backup.raw.trim()) {
      return;
    }

    const {
      raw,
      path
    } = backup;
    const label = getLabelForFileCollectionEntry(collection, path);
    return this.entryWithFormat(collection, slug)((0, _Entry.createEntry)(collection.get('name'), slug, path, {
      raw,
      label
    }));
  }

  async persistLocalDraftBackup(entry, collection) {
    const key = getEntryBackupKey(collection.get('name'), entry.get('slug'));
    const raw = this.entryToRaw(collection, entry);

    if (!raw.trim()) {
      return;
    }

    await _netlifyCmsLibUtil.localForage.setItem(key, {
      raw,
      path: entry.get('path')
    });
    return _netlifyCmsLibUtil.localForage.setItem(getEntryBackupKey(), raw);
  }

  async deleteLocalDraftBackup(collection, slug) {
    const key = getEntryBackupKey(collection.get('name'), slug);
    await _netlifyCmsLibUtil.localForage.removeItem(key);
    return this.deleteAnonymousBackup();
  } // Unnamed backup for use in the global error boundary, should always be
  // deleted on cms load.


  deleteAnonymousBackup() {
    return _netlifyCmsLibUtil.localForage.removeItem(getEntryBackupKey());
  }

  getEntry(collection, slug, ref) {
    const path = (0, _collections.selectEntryPath)(collection, slug);
    const label = getLabelForFileCollectionEntry(collection, path);
    return this.implementation.getEntry(collection, slug, path, ref).then(loadedEntry => this.entryWithFormat(collection, slug)((0, _Entry.createEntry)(collection.get('name'), slug, loadedEntry.file.path, {
      raw: loadedEntry.data,
      label,
      ref: loadedEntry.ref
    })));
  }

  async getFileVersions(collection, slug) {
    const path = (0, _collections.selectEntryPath)(collection, slug);
    const result = await this.implementation.getFileVersions(path);
    return dropUnrelatedCommits(result);
  }

  supportsFileVersioning() {
    return !!this.implementation.getFileVersions;
  }

  getMedia() {
    return this.implementation.getMedia();
  }

  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(collectionOrEntity) {
    return entry => {
      const format = (0, _formats.resolveFormat)(collectionOrEntity, 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);
    };
  }

  unpublishedEntries(collections) {
    return this.implementation.unpublishedEntries().then(loadedEntries => loadedEntries.filter(entry => entry !== null)).then(entries => entries.map(loadedEntry => {
      const entry = (0, _Entry.createEntry)(loadedEntry.metaData.collection, loadedEntry.slug, loadedEntry.file.path, {
        raw: loadedEntry.data,
        isModification: loadedEntry.isModification
      });
      entry.metaData = loadedEntry.metaData;
      return entry;
    })).then(entries => ({
      pagination: 0,
      entries: entries.reduce((acc, entry) => {
        const collection = collections.get(entry.collection);

        if (collection) {
          acc.push(this.entryWithFormat(collection)(entry));
        }

        return acc;
      }, [])
    }));
  }

  unpublishedEntry(collection, slug) {
    return this.implementation.unpublishedEntry(collection, slug).then(loadedEntry => {
      const entry = (0, _Entry.createEntry)('draft', loadedEntry.slug, loadedEntry.file.path, {
        raw: loadedEntry.data,
        isModification: loadedEntry.isModification
      });
      entry.metaData = loadedEntry.metaData;
      return entry;
    }).then(this.entryWithFormat(collection, slug));
  }
  /**
   * 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 undefiend or `show_preview_links` in the config is set to false, do nothing.
     */
    const baseUrl = this.config.get('site_url');

    if (!baseUrl || this.config.get('show_preview_links') === false) {
      return;
    }

    return {
      url: createPreviewUrl(baseUrl, collection, slug, this.config.get('slug'), entry),
      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) {
    let {
      maxAttempts = 1,
      interval = 5000
    } = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};

    /**
     * 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.get('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, slug);

      if (!deployPreview) {
        await new Promise(resolve => setTimeout(() => resolve(), interval));
      }
    }
    /**
     * If there's no deploy preview, do nothing.
     */


    if (!deployPreview) {
      return;
    }

    return {
      /**
       * Create a URL using the collection `preview_path`, if provided.
       */
      url: createPreviewUrl(deployPreview.url, collection, slug, this.config.get('slug'), entry),

      /**
       * Always capitalize the status for consistency.
       */
      status: deployPreview.status ? deployPreview.status.toUpperCase() : ''
    };
  }

  async persistEntry(config, collection, entryDraft, MediaFiles, integrations, usedSlugs) {
    let options = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : {
      revert: false
    };
    const {
      revert,
      ref
    } = options;
    const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false;
    const parsedData = {
      title: entryDraft.getIn(['entry', 'data', 'title'], 'No Title'),
      description: entryDraft.getIn(['entry', 'data', 'description'], 'No Description!')
    };
    let entryObj;

    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.get('slug'), usedSlugs);
      const path = (0, _collections.selectEntryPath)(collection, slug);
      entryObj = {
        path,
        slug,
        raw: this.entryToRaw(collection, entryDraft.get('entry'))
      };
    } else {
      const path = entryDraft.getIn(['entry', 'path']);
      const slug = entryDraft.getIn(['entry', 'slug']);
      entryObj = {
        path,
        slug,
        raw: this.entryToRaw(collection, entryDraft.get('entry'))
      };
    }

    const commitAction = revert ? 'revert' : newEntry ? 'create' : 'update';
    const commitMessage = commitMessageFormatter(commitAction, config, {
      collection,
      slug: entryObj.slug,
      path: entryObj.path,
      ref,
      revert
    });

    const useWorkflow = config.getIn(['publish_mode']) === _publishModes.EDITORIAL_WORKFLOW;

    const collectionName = collection.get('name');
    /**
     * Determine whether an asset store integration is in use.
     */

    const hasAssetStore = integrations && !!(0, _integrations.selectIntegration)(integrations, null, 'assetStore');

    const updatedOptions = _objectSpread({}, options, {
      hasAssetStore
    });

    const opts = _objectSpread({
      newEntry,
      parsedData,
      commitMessage,
      collectionName,
      useWorkflow
    }, updatedOptions); // NOTE AFTER CHANGING THE RETURN VALUE OF THIS - EDITORIAL WORKFLOW CONSUMERS MIGHT HAVE BEEN BROKEN


    return this.implementation.persistEntry(entryObj, MediaFiles, opts).then(results => {
      const [publishResponse] = results;
      return _objectSpread({
        slug: entryObj.slug
      }, publishResponse);
    });
  }

  persistMedia(config, file) {
    const options = {
      commitMessage: commitMessageFormatter('uploadMedia', config, {
        path: file.path
      })
    };
    return this.implementation.persistMedia(file, options);
  }

  deleteEntry(config, collection, slug) {
    const path = (0, _collections.selectEntryPath)(collection, slug);

    if (!(0, _collections.selectAllowDeletion)(collection)) {
      throw new Error('Not allowed to delete entries in this collection');
    }

    const commitMessage = commitMessageFormatter('delete', config, {
      collection,
      slug,
      path
    });
    return this.implementation.deleteFile(path, commitMessage, {
      collection,
      slug
    });
  }

  deleteMedia(config, path) {
    const commitMessage = commitMessageFormatter('deleteMedia', config, {
      path
    });
    return this.implementation.deleteFile(path, commitMessage);
  }

  persistUnpublishedEntry() {
    for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }

    return this.persistEntry(...args, {
      unpublished: true
    });
  }

  updateUnpublishedEntryStatus(collection, slug, newStatus) {
    return this.implementation.updateUnpublishedEntryStatus(collection, slug, newStatus);
  }

  publishUnpublishedEntry(collection, slug) {
    return this.implementation.publishUnpublishedEntry(collection, slug);
  }

  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);
    return format && format.toFile(entry.get('data').toJS(), fieldsOrder);
  }

  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 || []).filter(f => f.get('name') === entry.get('slug')).get(0);

    if (file == null) {
      throw new Error("No file found for ".concat(entry.get('slug'), " in ").concat(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) {
  const name = config.getIn(['backend', 'name']);

  if (name == null) {
    throw new Error('No backend defined in configuration');
  }

  const authStore = new LocalStorageAuthStore();

  if (!(0, _registry.getBackend)(name)) {
    throw new Error("Backend not found: ".concat(name));
  } else {
    return new Backend((0, _registry.getBackend)(name), {
      backendName: name,
      authStore,
      config
    });
  }
}

const currentBackend = function () {
  let backend = null;
  return config => {
    if (backend) {
      return backend;
    }

    if (config.get('backend')) {
      return backend = resolveBackend(config);
    }
  };
}();

exports.currentBackend = currentBackend;