"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); }; }();