@pranaysahith/decap-cms-core 3.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/README.md +9 -0
  2. package/dist/@pranaysahith/decap-cms-core.js +52 -0
  3. package/dist/@pranaysahith/decap-cms-core.js.LICENSE.txt +141 -0
  4. package/dist/@pranaysahith/decap-cms-core.js.map +1 -0
  5. package/dist/decap-cms-core.js +47 -0
  6. package/dist/decap-cms-core.js.LICENSE.txt +116 -0
  7. package/dist/decap-cms-core.js.map +1 -0
  8. package/dist/esm/actions/auth.js +97 -0
  9. package/dist/esm/actions/collections.js +15 -0
  10. package/dist/esm/actions/config.js +493 -0
  11. package/dist/esm/actions/deploys.js +79 -0
  12. package/dist/esm/actions/editorialWorkflow.js +480 -0
  13. package/dist/esm/actions/entries.js +865 -0
  14. package/dist/esm/actions/media.js +147 -0
  15. package/dist/esm/actions/mediaLibrary.js +552 -0
  16. package/dist/esm/actions/notifications.js +21 -0
  17. package/dist/esm/actions/search.js +149 -0
  18. package/dist/esm/actions/status.js +74 -0
  19. package/dist/esm/actions/waitUntil.js +32 -0
  20. package/dist/esm/backend.js +1082 -0
  21. package/dist/esm/bootstrap.js +101 -0
  22. package/dist/esm/components/App/App.js +289 -0
  23. package/dist/esm/components/App/Header.js +172 -0
  24. package/dist/esm/components/App/NotFoundPage.js +19 -0
  25. package/dist/esm/components/Collection/Collection.js +198 -0
  26. package/dist/esm/components/Collection/CollectionControls.js +46 -0
  27. package/dist/esm/components/Collection/CollectionSearch.js +222 -0
  28. package/dist/esm/components/Collection/CollectionTop.js +68 -0
  29. package/dist/esm/components/Collection/ControlButton.js +17 -0
  30. package/dist/esm/components/Collection/Entries/Entries.js +73 -0
  31. package/dist/esm/components/Collection/Entries/EntriesCollection.js +241 -0
  32. package/dist/esm/components/Collection/Entries/EntriesSearch.js +113 -0
  33. package/dist/esm/components/Collection/Entries/EntryCard.js +177 -0
  34. package/dist/esm/components/Collection/Entries/EntryListing.js +143 -0
  35. package/dist/esm/components/Collection/FilterControl.js +33 -0
  36. package/dist/esm/components/Collection/FolderRenameControl.js +403 -0
  37. package/dist/esm/components/Collection/GroupControl.js +33 -0
  38. package/dist/esm/components/Collection/NestedCollection.js +308 -0
  39. package/dist/esm/components/Collection/Sidebar.js +91 -0
  40. package/dist/esm/components/Collection/SortControl.js +59 -0
  41. package/dist/esm/components/Collection/ViewStyleControl.js +38 -0
  42. package/dist/esm/components/Editor/Editor.js +466 -0
  43. package/dist/esm/components/Editor/EditorControlPane/EditorControl.js +395 -0
  44. package/dist/esm/components/Editor/EditorControlPane/EditorControlPane.js +254 -0
  45. package/dist/esm/components/Editor/EditorControlPane/Widget.js +374 -0
  46. package/dist/esm/components/Editor/EditorInterface.js +386 -0
  47. package/dist/esm/components/Editor/EditorPreviewPane/EditorPreview.js +47 -0
  48. package/dist/esm/components/Editor/EditorPreviewPane/EditorPreviewContent.js +66 -0
  49. package/dist/esm/components/Editor/EditorPreviewPane/EditorPreviewPane.js +288 -0
  50. package/dist/esm/components/Editor/EditorPreviewPane/PreviewHOC.js +27 -0
  51. package/dist/esm/components/Editor/EditorToolbar.js +536 -0
  52. package/dist/esm/components/Editor/EntryPathEditor.js +272 -0
  53. package/dist/esm/components/Editor/withWorkflow.js +56 -0
  54. package/dist/esm/components/EditorWidgets/Unknown/UnknownControl.js +18 -0
  55. package/dist/esm/components/EditorWidgets/Unknown/UnknownPreview.js +20 -0
  56. package/dist/esm/components/EditorWidgets/index.js +4 -0
  57. package/dist/esm/components/MediaLibrary/EmptyMessage.js +22 -0
  58. package/dist/esm/components/MediaLibrary/MediaLibrary.js +446 -0
  59. package/dist/esm/components/MediaLibrary/MediaLibraryButtons.js +93 -0
  60. package/dist/esm/components/MediaLibrary/MediaLibraryCard.js +99 -0
  61. package/dist/esm/components/MediaLibrary/MediaLibraryCardGrid.js +198 -0
  62. package/dist/esm/components/MediaLibrary/MediaLibraryHeader.js +32 -0
  63. package/dist/esm/components/MediaLibrary/MediaLibraryModal.js +156 -0
  64. package/dist/esm/components/MediaLibrary/MediaLibrarySearch.js +51 -0
  65. package/dist/esm/components/MediaLibrary/MediaLibraryTop.js +123 -0
  66. package/dist/esm/components/UI/DragDrop.js +67 -0
  67. package/dist/esm/components/UI/ErrorBoundary.js +173 -0
  68. package/dist/esm/components/UI/FileUploadButton.js +27 -0
  69. package/dist/esm/components/UI/Modal.js +104 -0
  70. package/dist/esm/components/UI/Notifications.js +62 -0
  71. package/dist/esm/components/UI/SettingsDropdown.js +107 -0
  72. package/dist/esm/components/UI/index.js +6 -0
  73. package/dist/esm/components/Workflow/Workflow.js +133 -0
  74. package/dist/esm/components/Workflow/WorkflowCard.js +128 -0
  75. package/dist/esm/components/Workflow/WorkflowList.js +204 -0
  76. package/dist/esm/constants/collectionTypes.js +2 -0
  77. package/dist/esm/constants/collectionViews.js +2 -0
  78. package/dist/esm/constants/commitProps.js +2 -0
  79. package/dist/esm/constants/configSchema.js +644 -0
  80. package/dist/esm/constants/fieldInference.js +57 -0
  81. package/dist/esm/constants/publishModes.js +18 -0
  82. package/dist/esm/constants/validationErrorTypes.js +6 -0
  83. package/dist/esm/formats/formats.js +83 -0
  84. package/dist/esm/formats/frontmatter.js +146 -0
  85. package/dist/esm/formats/helpers.js +12 -0
  86. package/dist/esm/formats/json.js +8 -0
  87. package/dist/esm/formats/toml.js +32 -0
  88. package/dist/esm/formats/yaml.js +51 -0
  89. package/dist/esm/index.js +7 -0
  90. package/dist/esm/integrations/index.js +28 -0
  91. package/dist/esm/integrations/providers/algolia/implementation.js +174 -0
  92. package/dist/esm/integrations/providers/assetStore/implementation.js +165 -0
  93. package/dist/esm/lib/consoleError.js +3 -0
  94. package/dist/esm/lib/formatters.js +191 -0
  95. package/dist/esm/lib/i18n.js +367 -0
  96. package/dist/esm/lib/phrases.js +6 -0
  97. package/dist/esm/lib/polyfill.js +8 -0
  98. package/dist/esm/lib/registry.js +329 -0
  99. package/dist/esm/lib/serializeEntryValues.js +67 -0
  100. package/dist/esm/lib/stega.js +142 -0
  101. package/dist/esm/lib/textHelper.js +9 -0
  102. package/dist/esm/lib/urlHelper.js +111 -0
  103. package/dist/esm/mediaLibrary.js +37 -0
  104. package/dist/esm/reducers/auth.js +27 -0
  105. package/dist/esm/reducers/collections.js +428 -0
  106. package/dist/esm/reducers/combinedReducer.js +8 -0
  107. package/dist/esm/reducers/config.js +29 -0
  108. package/dist/esm/reducers/cursors.js +31 -0
  109. package/dist/esm/reducers/deploys.js +45 -0
  110. package/dist/esm/reducers/editorialWorkflow.js +83 -0
  111. package/dist/esm/reducers/entries.js +568 -0
  112. package/dist/esm/reducers/entryDraft.js +212 -0
  113. package/dist/esm/reducers/globalUI.js +25 -0
  114. package/dist/esm/reducers/index.js +66 -0
  115. package/dist/esm/reducers/integrations.js +53 -0
  116. package/dist/esm/reducers/mediaLibrary.js +252 -0
  117. package/dist/esm/reducers/medias.js +68 -0
  118. package/dist/esm/reducers/notifications.js +23 -0
  119. package/dist/esm/reducers/search.js +92 -0
  120. package/dist/esm/reducers/status.js +30 -0
  121. package/dist/esm/redux/index.js +7 -0
  122. package/dist/esm/redux/middleware/waitUntilAction.js +48 -0
  123. package/dist/esm/routing/history.js +12 -0
  124. package/dist/esm/types/diacritics.d.js +0 -0
  125. package/dist/esm/types/global.d.js +1 -0
  126. package/dist/esm/types/immutable.js +7 -0
  127. package/dist/esm/types/redux.js +14 -0
  128. package/dist/esm/types/tomlify-j0.4.d.js +0 -0
  129. package/dist/esm/valueObjects/AssetProxy.js +44 -0
  130. package/dist/esm/valueObjects/EditorComponent.js +34 -0
  131. package/dist/esm/valueObjects/Entry.js +20 -0
  132. package/index.d.ts +618 -0
  133. package/package.json +106 -0
  134. package/src/__tests__/backend.spec.js +1161 -0
  135. package/src/actions/__tests__/config.spec.js +1009 -0
  136. package/src/actions/__tests__/editorialWorkflow.spec.js +216 -0
  137. package/src/actions/__tests__/entries.spec.js +596 -0
  138. package/src/actions/__tests__/media.spec.ts +171 -0
  139. package/src/actions/__tests__/mediaLibrary.spec.js +327 -0
  140. package/src/actions/__tests__/search.spec.js +209 -0
  141. package/src/actions/auth.ts +127 -0
  142. package/src/actions/collections.ts +18 -0
  143. package/src/actions/config.ts +565 -0
  144. package/src/actions/deploys.ts +104 -0
  145. package/src/actions/editorialWorkflow.ts +567 -0
  146. package/src/actions/entries.ts +1055 -0
  147. package/src/actions/media.ts +139 -0
  148. package/src/actions/mediaLibrary.ts +574 -0
  149. package/src/actions/notifications.ts +36 -0
  150. package/src/actions/search.ts +221 -0
  151. package/src/actions/status.ts +99 -0
  152. package/src/actions/waitUntil.ts +49 -0
  153. package/src/backend.ts +1400 -0
  154. package/src/bootstrap.js +104 -0
  155. package/src/components/App/App.js +286 -0
  156. package/src/components/App/Header.js +266 -0
  157. package/src/components/App/NotFoundPage.js +23 -0
  158. package/src/components/Collection/Collection.js +210 -0
  159. package/src/components/Collection/CollectionControls.js +58 -0
  160. package/src/components/Collection/CollectionSearch.js +243 -0
  161. package/src/components/Collection/CollectionTop.js +81 -0
  162. package/src/components/Collection/ControlButton.js +27 -0
  163. package/src/components/Collection/Entries/Entries.js +82 -0
  164. package/src/components/Collection/Entries/EntriesCollection.js +277 -0
  165. package/src/components/Collection/Entries/EntriesSearch.js +102 -0
  166. package/src/components/Collection/Entries/EntryCard.js +246 -0
  167. package/src/components/Collection/Entries/EntryListing.js +151 -0
  168. package/src/components/Collection/Entries/__tests__/EntriesCollection.spec.js +163 -0
  169. package/src/components/Collection/Entries/__tests__/__snapshots__/EntriesCollection.spec.js.snap +46 -0
  170. package/src/components/Collection/FilterControl.js +39 -0
  171. package/src/components/Collection/GroupControl.js +39 -0
  172. package/src/components/Collection/NestedCollection.js +330 -0
  173. package/src/components/Collection/Sidebar.js +136 -0
  174. package/src/components/Collection/SortControl.js +68 -0
  175. package/src/components/Collection/ViewStyleControl.js +50 -0
  176. package/src/components/Collection/__tests__/Collection.spec.js +75 -0
  177. package/src/components/Collection/__tests__/NestedCollection.spec.js +445 -0
  178. package/src/components/Collection/__tests__/Sidebar.spec.js +87 -0
  179. package/src/components/Collection/__tests__/__snapshots__/Collection.spec.js.snap +144 -0
  180. package/src/components/Collection/__tests__/__snapshots__/NestedCollection.spec.js.snap +550 -0
  181. package/src/components/Collection/__tests__/__snapshots__/Sidebar.spec.js.snap +312 -0
  182. package/src/components/Editor/Editor.js +497 -0
  183. package/src/components/Editor/EditorControlPane/EditorControl.js +452 -0
  184. package/src/components/Editor/EditorControlPane/EditorControlPane.js +269 -0
  185. package/src/components/Editor/EditorControlPane/Widget.js +384 -0
  186. package/src/components/Editor/EditorInterface.js +444 -0
  187. package/src/components/Editor/EditorPreviewPane/EditorPreview.js +40 -0
  188. package/src/components/Editor/EditorPreviewPane/EditorPreviewContent.js +74 -0
  189. package/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js +333 -0
  190. package/src/components/Editor/EditorPreviewPane/PreviewHOC.js +33 -0
  191. package/src/components/Editor/EditorToolbar.js +691 -0
  192. package/src/components/Editor/__tests__/Editor.spec.js +221 -0
  193. package/src/components/Editor/__tests__/EditorToolbar.spec.js +120 -0
  194. package/src/components/Editor/__tests__/__snapshots__/Editor.spec.js.snap +45 -0
  195. package/src/components/Editor/__tests__/__snapshots__/EditorToolbar.spec.js.snap +4233 -0
  196. package/src/components/Editor/withWorkflow.js +61 -0
  197. package/src/components/EditorWidgets/Unknown/UnknownControl.js +17 -0
  198. package/src/components/EditorWidgets/Unknown/UnknownPreview.js +19 -0
  199. package/src/components/EditorWidgets/index.js +5 -0
  200. package/src/components/MediaLibrary/EmptyMessage.js +28 -0
  201. package/src/components/MediaLibrary/MediaLibrary.js +411 -0
  202. package/src/components/MediaLibrary/MediaLibraryButtons.js +136 -0
  203. package/src/components/MediaLibrary/MediaLibraryCard.js +128 -0
  204. package/src/components/MediaLibrary/MediaLibraryCardGrid.js +199 -0
  205. package/src/components/MediaLibrary/MediaLibraryHeader.js +48 -0
  206. package/src/components/MediaLibrary/MediaLibraryModal.js +200 -0
  207. package/src/components/MediaLibrary/MediaLibrarySearch.js +61 -0
  208. package/src/components/MediaLibrary/MediaLibraryTop.js +143 -0
  209. package/src/components/MediaLibrary/__tests__/MediaLibraryButtons.spec.js +45 -0
  210. package/src/components/MediaLibrary/__tests__/MediaLibraryCard.spec.js +49 -0
  211. package/src/components/MediaLibrary/__tests__/__snapshots__/MediaLibraryCard.spec.js.snap +264 -0
  212. package/src/components/UI/DragDrop.js +66 -0
  213. package/src/components/UI/ErrorBoundary.js +214 -0
  214. package/src/components/UI/FileUploadButton.js +24 -0
  215. package/src/components/UI/Modal.js +112 -0
  216. package/src/components/UI/Notifications.tsx +83 -0
  217. package/src/components/UI/SettingsDropdown.js +103 -0
  218. package/src/components/UI/__tests__/ErrorBoundary.spec.js +57 -0
  219. package/src/components/UI/index.js +6 -0
  220. package/src/components/Workflow/Workflow.js +169 -0
  221. package/src/components/Workflow/WorkflowCard.js +177 -0
  222. package/src/components/Workflow/WorkflowList.js +272 -0
  223. package/src/constants/__tests__/configSchema.spec.js +611 -0
  224. package/src/constants/collectionTypes.ts +2 -0
  225. package/src/constants/collectionViews.js +2 -0
  226. package/src/constants/commitProps.ts +2 -0
  227. package/src/constants/configSchema.js +441 -0
  228. package/src/constants/fieldInference.tsx +78 -0
  229. package/src/constants/publishModes.ts +22 -0
  230. package/src/constants/validationErrorTypes.js +6 -0
  231. package/src/formats/__tests__/formats.spec.js +87 -0
  232. package/src/formats/__tests__/frontmatter.spec.js +429 -0
  233. package/src/formats/__tests__/toml.spec.js +9 -0
  234. package/src/formats/__tests__/yaml.spec.js +162 -0
  235. package/src/formats/formats.ts +97 -0
  236. package/src/formats/frontmatter.ts +150 -0
  237. package/src/formats/helpers.ts +14 -0
  238. package/src/formats/json.ts +9 -0
  239. package/src/formats/toml.ts +33 -0
  240. package/src/formats/yaml.ts +58 -0
  241. package/src/index.js +8 -0
  242. package/src/integrations/index.js +35 -0
  243. package/src/integrations/providers/algolia/implementation.js +176 -0
  244. package/src/integrations/providers/assetStore/implementation.js +148 -0
  245. package/src/lib/__tests__/formatters.spec.js +751 -0
  246. package/src/lib/__tests__/i18n.spec.js +792 -0
  247. package/src/lib/__tests__/phrases.spec.js +119 -0
  248. package/src/lib/__tests__/registry.spec.js +261 -0
  249. package/src/lib/__tests__/serializeEntryValues.spec.js +22 -0
  250. package/src/lib/__tests__/urlHelper.spec.js +138 -0
  251. package/src/lib/consoleError.js +7 -0
  252. package/src/lib/formatters.ts +286 -0
  253. package/src/lib/i18n.ts +454 -0
  254. package/src/lib/phrases.js +8 -0
  255. package/src/lib/polyfill.js +9 -0
  256. package/src/lib/registry.js +312 -0
  257. package/src/lib/serializeEntryValues.js +75 -0
  258. package/src/lib/stega.ts +145 -0
  259. package/src/lib/textHelper.js +11 -0
  260. package/src/lib/urlHelper.ts +128 -0
  261. package/src/mediaLibrary.ts +51 -0
  262. package/src/reducers/__tests__/auth.spec.ts +38 -0
  263. package/src/reducers/__tests__/collections.spec.js +610 -0
  264. package/src/reducers/__tests__/config.spec.js +38 -0
  265. package/src/reducers/__tests__/entries.spec.js +694 -0
  266. package/src/reducers/__tests__/entryDraft.spec.js +297 -0
  267. package/src/reducers/__tests__/globalUI.js +43 -0
  268. package/src/reducers/__tests__/integrations.spec.ts +76 -0
  269. package/src/reducers/__tests__/mediaLibrary.spec.js +154 -0
  270. package/src/reducers/__tests__/medias.spec.ts +49 -0
  271. package/src/reducers/auth.ts +46 -0
  272. package/src/reducers/collections.ts +535 -0
  273. package/src/reducers/combinedReducer.ts +11 -0
  274. package/src/reducers/config.ts +38 -0
  275. package/src/reducers/cursors.js +36 -0
  276. package/src/reducers/deploys.ts +52 -0
  277. package/src/reducers/editorialWorkflow.ts +163 -0
  278. package/src/reducers/entries.ts +819 -0
  279. package/src/reducers/entryDraft.js +260 -0
  280. package/src/reducers/globalUI.ts +45 -0
  281. package/src/reducers/index.ts +82 -0
  282. package/src/reducers/integrations.ts +59 -0
  283. package/src/reducers/mediaLibrary.ts +296 -0
  284. package/src/reducers/medias.ts +66 -0
  285. package/src/reducers/notifications.ts +52 -0
  286. package/src/reducers/search.ts +111 -0
  287. package/src/reducers/status.ts +40 -0
  288. package/src/redux/index.ts +18 -0
  289. package/src/redux/middleware/waitUntilAction.ts +64 -0
  290. package/src/routing/__tests__/history.spec.ts +49 -0
  291. package/src/routing/history.ts +17 -0
  292. package/src/types/diacritics.d.ts +1 -0
  293. package/src/types/global.d.ts +8 -0
  294. package/src/types/immutable.ts +49 -0
  295. package/src/types/redux.ts +827 -0
  296. package/src/types/tomlify-j0.4.d.ts +13 -0
  297. package/src/valueObjects/AssetProxy.ts +48 -0
  298. package/src/valueObjects/EditorComponent.js +38 -0
  299. package/src/valueObjects/Entry.ts +63 -0
package/src/backend.ts ADDED
@@ -0,0 +1,1400 @@
1
+ import attempt from 'lodash/attempt';
2
+ import flatten from 'lodash/flatten';
3
+ import isError from 'lodash/isError';
4
+ import uniq from 'lodash/uniq';
5
+ import trim from 'lodash/trim';
6
+ import sortBy from 'lodash/sortBy';
7
+ import get from 'lodash/get';
8
+ import set from 'lodash/set';
9
+ import { List, fromJS, Set } from 'immutable';
10
+ import * as fuzzy from 'fuzzy';
11
+ import {
12
+ localForage,
13
+ Cursor,
14
+ CURSOR_COMPATIBILITY_SYMBOL,
15
+ getPathDepth,
16
+ blobToFileObj,
17
+ asyncLock,
18
+ EDITORIAL_WORKFLOW_ERROR,
19
+ } from 'decap-cms-lib-util';
20
+ import { basename, join, extname, dirname } from 'path';
21
+ import { stringTemplate } from 'decap-cms-lib-widgets';
22
+
23
+ import { resolveFormat } from './formats/formats';
24
+ import { selectUseWorkflow } from './reducers/config';
25
+ import { selectMediaFilePath, selectEntry } from './reducers/entries';
26
+ import { selectIntegration } from './reducers/integrations';
27
+ import {
28
+ selectEntrySlug,
29
+ selectEntryPath,
30
+ selectFileEntryLabel,
31
+ selectAllowNewEntries,
32
+ selectAllowDeletion,
33
+ selectFolderEntryExtension,
34
+ selectInferredField,
35
+ selectMediaFolders,
36
+ selectFieldsComments,
37
+ selectHasMetaPath,
38
+ } from './reducers/collections';
39
+ import { createEntry } from './valueObjects/Entry';
40
+ import { sanitizeChar } from './lib/urlHelper';
41
+ import { getBackend, invokeEvent } from './lib/registry';
42
+ import { commitMessageFormatter, slugFormatter, previewUrlFormatter } from './lib/formatters';
43
+ import { status } from './constants/publishModes';
44
+ import { FOLDER, FILES } from './constants/collectionTypes';
45
+ import { selectCustomPath } from './reducers/entryDraft';
46
+ import {
47
+ getI18nFilesDepth,
48
+ getI18nFiles,
49
+ hasI18n,
50
+ getFilePaths,
51
+ getI18nEntry,
52
+ groupEntries,
53
+ getI18nDataFiles,
54
+ getI18nBackup,
55
+ formatI18nBackup,
56
+ getI18nInfo,
57
+ I18N_STRUCTURE,
58
+ } from './lib/i18n';
59
+
60
+ import type { I18nInfo } from './lib/i18n';
61
+ import type AssetProxy from './valueObjects/AssetProxy';
62
+ import type {
63
+ CmsConfig,
64
+ EntryMap,
65
+ FilterRule,
66
+ EntryDraft,
67
+ Collection,
68
+ Collections,
69
+ CollectionFile,
70
+ State,
71
+ EntryField,
72
+ } from './types/redux';
73
+ import type { EntryValue } from './valueObjects/Entry';
74
+ import type {
75
+ Implementation as BackendImplementation,
76
+ DisplayURL,
77
+ ImplementationEntry,
78
+ Credentials,
79
+ User,
80
+ AsyncLock,
81
+ UnpublishedEntry,
82
+ DataFile,
83
+ UnpublishedEntryDiff,
84
+ } from 'decap-cms-lib-util';
85
+ import type { Map } from 'immutable';
86
+
87
+ const { extractTemplateVars, dateParsers, expandPath } = stringTemplate;
88
+
89
+ function updateAssetProxies(
90
+ assetProxies: AssetProxy[],
91
+ config: CmsConfig,
92
+ collection: Collection,
93
+ entryDraft: EntryDraft,
94
+ path: string,
95
+ ) {
96
+ assetProxies.map(asset => {
97
+ // update media files path based on entry path
98
+ const oldPath = asset.path;
99
+ const newPath = selectMediaFilePath(
100
+ config,
101
+ collection,
102
+ entryDraft.get('entry').set('path', path),
103
+ oldPath,
104
+ asset.field,
105
+ );
106
+ asset.path = newPath;
107
+ });
108
+ }
109
+
110
+ export class LocalStorageAuthStore {
111
+ storageKey = 'decap-cms-user';
112
+
113
+ retrieve() {
114
+ const data = window.localStorage.getItem(this.storageKey);
115
+ return data && JSON.parse(data);
116
+ }
117
+
118
+ store(userData: unknown) {
119
+ window.localStorage.setItem(this.storageKey, JSON.stringify(userData));
120
+ }
121
+
122
+ logout() {
123
+ window.localStorage.removeItem(this.storageKey);
124
+ }
125
+ }
126
+
127
+ function getEntryBackupKey(collectionName?: string, slug?: string) {
128
+ const baseKey = 'backup';
129
+ if (!collectionName) {
130
+ return baseKey;
131
+ }
132
+ const suffix = slug ? `.${slug}` : '';
133
+ return `${baseKey}.${collectionName}${suffix}`;
134
+ }
135
+
136
+ function getEntryField(field: string, entry: EntryValue) {
137
+ const value = get(entry.data, field);
138
+ if (value) {
139
+ return String(value);
140
+ } else {
141
+ const firstFieldPart = field.split('.')[0];
142
+ if (entry[firstFieldPart as keyof EntryValue]) {
143
+ // allows searching using entry.slug/entry.path etc.
144
+ return entry[firstFieldPart as keyof EntryValue];
145
+ } else {
146
+ return '';
147
+ }
148
+ }
149
+ }
150
+
151
+ export function extractSearchFields(searchFields: string[]) {
152
+ return (entry: EntryValue) =>
153
+ searchFields.reduce((acc, field) => {
154
+ const value = getEntryField(field, entry);
155
+ if (value) {
156
+ return `${acc} ${value}`;
157
+ } else {
158
+ return acc;
159
+ }
160
+ }, '');
161
+ }
162
+
163
+ export function expandSearchEntries(entries: EntryValue[], searchFields: string[]) {
164
+ // expand the entries for the purpose of the search
165
+ const expandedEntries = entries.reduce((acc, e) => {
166
+ const expandedFields = searchFields.reduce((acc, f) => {
167
+ const fields = expandPath({ data: e.data, path: f });
168
+ acc.push(...fields);
169
+ return acc;
170
+ }, [] as string[]);
171
+
172
+ for (let i = 0; i < expandedFields.length; i++) {
173
+ acc.push({ ...e, field: expandedFields[i] });
174
+ }
175
+
176
+ return acc;
177
+ }, [] as (EntryValue & { field: string })[]);
178
+
179
+ return expandedEntries;
180
+ }
181
+
182
+ export function mergeExpandedEntries(entries: (EntryValue & { field: string })[]) {
183
+ // merge the search results by slug and only keep data that matched the search
184
+ const fields = entries.map(f => f.field);
185
+ const arrayPaths: Record<string, Set<string>> = {};
186
+
187
+ const merged = entries.reduce((acc, e) => {
188
+ if (!acc[e.slug]) {
189
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
190
+ const { field, ...rest } = e;
191
+ acc[e.slug] = rest;
192
+ arrayPaths[e.slug] = Set();
193
+ }
194
+
195
+ const nestedFields = e.field.split('.');
196
+ let value = acc[e.slug].data;
197
+ for (let i = 0; i < nestedFields.length; i++) {
198
+ value = value[nestedFields[i]];
199
+ if (Array.isArray(value)) {
200
+ const path = nestedFields.slice(0, i + 1).join('.');
201
+ arrayPaths[e.slug] = arrayPaths[e.slug].add(path);
202
+ }
203
+ }
204
+
205
+ return acc;
206
+ }, {} as Record<string, EntryValue>);
207
+
208
+ // this keeps the search score sorting order designated by the order in entries
209
+ // and filters non matching items
210
+ Object.keys(merged).forEach(slug => {
211
+ const data = merged[slug].data;
212
+ for (const path of arrayPaths[slug].toArray()) {
213
+ const array = get(data, path) as unknown[];
214
+ const filtered = array.filter((_, index) => {
215
+ return fields.some(f => `${f}.`.startsWith(`${path}.${index}.`));
216
+ });
217
+ filtered.sort((a, b) => {
218
+ const indexOfA = array.indexOf(a);
219
+ const indexOfB = array.indexOf(b);
220
+ const pathOfA = `${path}.${indexOfA}.`;
221
+ const pathOfB = `${path}.${indexOfB}.`;
222
+
223
+ const matchingFieldIndexA = fields.findIndex(f => `${f}.`.startsWith(pathOfA));
224
+ const matchingFieldIndexB = fields.findIndex(f => `${f}.`.startsWith(pathOfB));
225
+
226
+ return matchingFieldIndexA - matchingFieldIndexB;
227
+ });
228
+
229
+ set(data, path, filtered);
230
+ }
231
+ });
232
+
233
+ return Object.values(merged);
234
+ }
235
+
236
+ function sortByScore(a: fuzzy.FilterResult<EntryValue>, b: fuzzy.FilterResult<EntryValue>) {
237
+ if (a.score > b.score) return -1;
238
+ if (a.score < b.score) return 1;
239
+ return 0;
240
+ }
241
+
242
+ export function slugFromCustomPath(collection: Collection, customPath: string) {
243
+ const folderPath = collection.get('folder', '') as string;
244
+ const entryPath = customPath.toLowerCase().replace(folderPath.toLowerCase(), '');
245
+ const slug = join(dirname(trim(entryPath, '/')), basename(entryPath, extname(customPath)));
246
+ return slug;
247
+ }
248
+
249
+ interface AuthStore {
250
+ retrieve: () => User;
251
+ store: (user: User) => void;
252
+ logout: () => void;
253
+ }
254
+
255
+ interface BackendOptions {
256
+ backendName: string;
257
+ config: CmsConfig;
258
+ authStore?: AuthStore;
259
+ }
260
+
261
+ export interface MediaFile {
262
+ name: string;
263
+ id: string;
264
+ size?: number;
265
+ displayURL?: DisplayURL;
266
+ path: string;
267
+ draft?: boolean;
268
+ url?: string;
269
+ file?: File;
270
+ field?: EntryField;
271
+ }
272
+
273
+ interface BackupEntry {
274
+ raw: string;
275
+ path: string;
276
+ mediaFiles: MediaFile[];
277
+ i18n?: Record<string, { raw: string }>;
278
+ }
279
+
280
+ interface PersistArgs {
281
+ config: CmsConfig;
282
+ collection: Collection;
283
+ entryDraft: EntryDraft;
284
+ assetProxies: AssetProxy[];
285
+ usedSlugs: List<string>;
286
+ unpublished?: boolean;
287
+ status?: string;
288
+ }
289
+
290
+ interface ImplementationInitOptions {
291
+ useWorkflow: boolean;
292
+ updateUserCredentials: (credentials: Credentials) => void;
293
+ initialWorkflowStatus: string;
294
+ }
295
+
296
+ type Implementation = BackendImplementation & {
297
+ init: (config: CmsConfig, options: ImplementationInitOptions) => Implementation;
298
+ };
299
+
300
+ function prepareMetaPath(path: string, collection: Collection) {
301
+ if (!selectHasMetaPath(collection)) {
302
+ return path;
303
+ }
304
+ const dir = dirname(path);
305
+ return dir.slice(collection.get('folder')!.length + 1) || '/';
306
+ }
307
+
308
+ function collectionDepth(collection: Collection) {
309
+ let depth;
310
+ depth =
311
+ collection.get('nested')?.get('depth') || getPathDepth(collection.get('path', '') as string);
312
+
313
+ if (hasI18n(collection)) {
314
+ depth = getI18nFilesDepth(collection, depth);
315
+ }
316
+
317
+ return depth;
318
+ }
319
+
320
+ function i18nRulestring(ruleString: string, { defaultLocale, structure }: I18nInfo): string {
321
+ if (structure === I18N_STRUCTURE.MULTIPLE_FOLDERS) {
322
+ return `${defaultLocale}\\/${ruleString}`;
323
+ }
324
+
325
+ if (structure === I18N_STRUCTURE.MULTIPLE_FILES) {
326
+ return `${ruleString}\\.${defaultLocale}\\..*`;
327
+ }
328
+
329
+ return ruleString;
330
+ }
331
+
332
+ function collectionRegex(collection: Collection): RegExp | undefined {
333
+ let ruleString = '';
334
+
335
+ if (collection.get('path')) {
336
+ ruleString = `${collection.get('folder')}/${collection.get('path')}`.replace(
337
+ /{{.*}}/gm,
338
+ '(.*)',
339
+ );
340
+ }
341
+
342
+ if (hasI18n(collection)) {
343
+ ruleString = i18nRulestring(ruleString, getI18nInfo(collection) as I18nInfo);
344
+ }
345
+
346
+ return ruleString ? new RegExp(ruleString) : undefined;
347
+ }
348
+
349
+ export class Backend {
350
+ implementation: Implementation;
351
+ backendName: string;
352
+ config: CmsConfig;
353
+ authStore?: AuthStore;
354
+ user?: User | null;
355
+ backupSync: AsyncLock;
356
+
357
+ constructor(implementation: Implementation, { backendName, authStore, config }: BackendOptions) {
358
+ // We can't reliably run this on exit, so we do cleanup on load.
359
+ this.deleteAnonymousBackup();
360
+ this.config = config;
361
+ this.implementation = implementation.init(this.config, {
362
+ useWorkflow: selectUseWorkflow(this.config),
363
+ updateUserCredentials: this.updateUserCredentials,
364
+ initialWorkflowStatus: status.first(),
365
+ });
366
+ this.backendName = backendName;
367
+ this.authStore = authStore;
368
+ if (this.implementation === null) {
369
+ throw new Error('Cannot instantiate a Backend with no implementation');
370
+ }
371
+ this.backupSync = asyncLock();
372
+ }
373
+
374
+ async status() {
375
+ const attempts = 3;
376
+ let status: {
377
+ auth: { status: boolean };
378
+ api: { status: boolean; statusPage: string };
379
+ } = {
380
+ auth: { status: true },
381
+ api: { status: true, statusPage: '' },
382
+ };
383
+ for (let i = 1; i <= attempts; i++) {
384
+ status = await this.implementation.status();
385
+ // return on first success
386
+ if (Object.values(status).every(s => s.status === true)) {
387
+ return status;
388
+ } else {
389
+ await new Promise(resolve => setTimeout(resolve, i * 1000));
390
+ }
391
+ }
392
+ return status;
393
+ }
394
+
395
+ currentUser() {
396
+ if (this.user) {
397
+ return this.user;
398
+ }
399
+ const stored = this.authStore!.retrieve();
400
+ if (stored && stored.backendName === this.backendName) {
401
+ return Promise.resolve(this.implementation.restoreUser(stored)).then(user => {
402
+ this.user = { ...user, backendName: this.backendName };
403
+ // return confirmed/rehydrated user object instead of stored
404
+ this.authStore!.store(this.user as User);
405
+ return this.user;
406
+ });
407
+ }
408
+ return Promise.resolve(null);
409
+ }
410
+
411
+ isGitBackend() {
412
+ return this.implementation.isGitBackend?.() || false;
413
+ }
414
+
415
+ updateUserCredentials = (updatedCredentials: Credentials) => {
416
+ const storedUser = this.authStore!.retrieve();
417
+ if (storedUser && storedUser.backendName === this.backendName) {
418
+ this.user = { ...storedUser, ...updatedCredentials };
419
+ this.authStore!.store(this.user as User);
420
+ return this.user;
421
+ }
422
+ };
423
+
424
+ authComponent() {
425
+ return this.implementation.authComponent();
426
+ }
427
+
428
+ authenticate(credentials: Credentials) {
429
+ return this.implementation.authenticate(credentials).then(user => {
430
+ this.user = { ...user, backendName: this.backendName };
431
+ if (this.authStore) {
432
+ this.authStore.store(this.user as User);
433
+ }
434
+ return this.user;
435
+ });
436
+ }
437
+
438
+ async logout() {
439
+ try {
440
+ await this.implementation.logout();
441
+ } catch (e) {
442
+ console.warn('Error during logout', e.message);
443
+ } finally {
444
+ this.user = null;
445
+ if (this.authStore) {
446
+ this.authStore.logout();
447
+ }
448
+ }
449
+ }
450
+
451
+ getToken = () => this.implementation.getToken();
452
+
453
+ async entryExist(collection: Collection, path: string, slug: string, useWorkflow: boolean) {
454
+ const unpublishedEntry =
455
+ useWorkflow &&
456
+ (await this.implementation
457
+ .unpublishedEntry({ collection: collection.get('name'), slug })
458
+ .catch(error => {
459
+ if (error.name === EDITORIAL_WORKFLOW_ERROR && error.notUnderEditorialWorkflow) {
460
+ return Promise.resolve(false);
461
+ }
462
+ return Promise.reject(error);
463
+ }));
464
+
465
+ if (unpublishedEntry) return unpublishedEntry;
466
+
467
+ const publishedEntry = await this.implementation
468
+ .getEntry(path)
469
+ .then(({ data }) => data)
470
+ .catch(() => {
471
+ return Promise.resolve(false);
472
+ });
473
+
474
+ return publishedEntry;
475
+ }
476
+
477
+ async generateUniqueSlug(
478
+ collection: Collection,
479
+ entryData: Map<string, unknown>,
480
+ config: CmsConfig,
481
+ usedSlugs: List<string>,
482
+ customPath: string | undefined,
483
+ ) {
484
+ const slugConfig = config.slug;
485
+ let slug: string;
486
+ if (customPath) {
487
+ slug = slugFromCustomPath(collection, customPath);
488
+ } else {
489
+ slug = slugFormatter(collection, entryData, slugConfig);
490
+ }
491
+ let i = 1;
492
+ let uniqueSlug = slug;
493
+
494
+ // Check for duplicate slug in loaded entities store first before repo
495
+ while (
496
+ usedSlugs.includes(uniqueSlug) ||
497
+ (await this.entryExist(
498
+ collection,
499
+ selectEntryPath(collection, uniqueSlug) as string,
500
+ uniqueSlug,
501
+ selectUseWorkflow(config),
502
+ ))
503
+ ) {
504
+ uniqueSlug = `${slug}${sanitizeChar(' ', slugConfig)}${i++}`;
505
+ }
506
+ return uniqueSlug;
507
+ }
508
+
509
+ processEntries(loadedEntries: ImplementationEntry[], collection: Collection) {
510
+ const entries = loadedEntries.map(loadedEntry =>
511
+ createEntry(
512
+ collection.get('name'),
513
+ selectEntrySlug(collection, loadedEntry.file.path),
514
+ loadedEntry.file.path,
515
+ {
516
+ raw: loadedEntry.data || '',
517
+ label: loadedEntry.file.label,
518
+ author: loadedEntry.file.author,
519
+ updatedOn: loadedEntry.file.updatedOn,
520
+ meta: { path: prepareMetaPath(loadedEntry.file.path, collection) },
521
+ },
522
+ ),
523
+ );
524
+ const formattedEntries = entries.map(this.entryWithFormat(collection));
525
+ // If this collection has a "filter" property, filter entries accordingly
526
+ const collectionFilter = collection.get('filter');
527
+ const filteredEntries = collectionFilter
528
+ ? this.filterEntries({ entries: formattedEntries }, collectionFilter)
529
+ : formattedEntries;
530
+
531
+ if (hasI18n(collection)) {
532
+ const extension = selectFolderEntryExtension(collection);
533
+ const groupedEntries = groupEntries(collection, extension, filteredEntries);
534
+ return groupedEntries;
535
+ }
536
+
537
+ return filteredEntries;
538
+ }
539
+
540
+ async listEntries(collection: Collection) {
541
+ const extension = selectFolderEntryExtension(collection);
542
+ let listMethod: () => Promise<ImplementationEntry[]>;
543
+ const collectionType = collection.get('type');
544
+ if (collectionType === FOLDER) {
545
+ listMethod = () => {
546
+ const depth = collectionDepth(collection);
547
+ return this.implementation.entriesByFolder(
548
+ collection.get('folder') as string,
549
+ extension,
550
+ depth,
551
+ );
552
+ };
553
+ } else if (collectionType === FILES) {
554
+ const files = collection
555
+ .get('files')!
556
+ .map(collectionFile => ({
557
+ path: collectionFile!.get('file'),
558
+ label: collectionFile!.get('label'),
559
+ }))
560
+ .toArray();
561
+ listMethod = () => this.implementation.entriesByFiles(files);
562
+ } else {
563
+ throw new Error(`Unknown collection type: ${collectionType}`);
564
+ }
565
+ const loadedEntries = await listMethod();
566
+ /*
567
+ Wrap cursors so we can tell which collection the cursor is
568
+ from. This is done to prevent traverseCursor from requiring a
569
+ `collection` argument.
570
+ */
571
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
572
+ // @ts-ignore
573
+ const cursor = Cursor.create(loadedEntries[CURSOR_COMPATIBILITY_SYMBOL]).wrapData({
574
+ cursorType: 'collectionEntries',
575
+ collection,
576
+ });
577
+ return {
578
+ entries: this.processEntries(loadedEntries, collection),
579
+ pagination: cursor.meta?.get('page'),
580
+ cursor,
581
+ };
582
+ }
583
+
584
+ // The same as listEntries, except that if a cursor with the "next"
585
+ // action available is returned, it calls "next" on the cursor and
586
+ // repeats the process. Once there is no available "next" action, it
587
+ // returns all the collected entries. Used to retrieve all entries
588
+ // for local searches and queries.
589
+ async listAllEntries(collection: Collection) {
590
+ if (collection.get('folder') && this.implementation.allEntriesByFolder) {
591
+ const depth = collectionDepth(collection);
592
+ const extension = selectFolderEntryExtension(collection);
593
+ return this.implementation
594
+ .allEntriesByFolder(
595
+ collection.get('folder') as string,
596
+ extension,
597
+ depth,
598
+ collectionRegex(collection),
599
+ )
600
+ .then(entries => this.processEntries(entries, collection));
601
+ }
602
+
603
+ const response = await this.listEntries(collection);
604
+ const { entries } = response;
605
+ let { cursor } = response;
606
+ while (cursor && cursor.actions!.includes('next')) {
607
+ const { entries: newEntries, cursor: newCursor } = await this.traverseCursor(cursor, 'next');
608
+ entries.push(...newEntries);
609
+ cursor = newCursor;
610
+ }
611
+ return entries;
612
+ }
613
+
614
+ async search(collections: Collection[], searchTerm: string) {
615
+ // Perform a local search by requesting all entries. For each
616
+ // collection, load it, search, and call onCollectionResults with
617
+ // its results.
618
+ const errors: Error[] = [];
619
+ const collectionEntriesRequests = collections
620
+ .map(async collection => {
621
+ const summary = collection.get('summary', '') as string;
622
+ const summaryFields = extractTemplateVars(summary);
623
+
624
+ // TODO: pass search fields in as an argument
625
+ let searchFields: (string | null | undefined)[] = [];
626
+
627
+ if (collection.get('type') === FILES) {
628
+ collection.get('files')?.forEach(f => {
629
+ const topLevelFields = f!
630
+ .get('fields')
631
+ .map(f => f!.get('name'))
632
+ .toArray();
633
+ searchFields = [...searchFields, ...topLevelFields];
634
+ });
635
+ } else {
636
+ searchFields = [
637
+ selectInferredField(collection, 'title'),
638
+ selectInferredField(collection, 'shortTitle'),
639
+ selectInferredField(collection, 'author'),
640
+ ...summaryFields.map(elem => {
641
+ if (dateParsers[elem]) {
642
+ return selectInferredField(collection, 'date');
643
+ }
644
+ return elem;
645
+ }),
646
+ ];
647
+ }
648
+ const filteredSearchFields = searchFields.filter(Boolean) as string[];
649
+ const collectionEntries = await this.listAllEntries(collection);
650
+ return fuzzy.filter(searchTerm, collectionEntries, {
651
+ extract: extractSearchFields(uniq(filteredSearchFields)),
652
+ });
653
+ })
654
+ .map(p =>
655
+ p.catch(err => {
656
+ errors.push(err);
657
+ return [] as fuzzy.FilterResult<EntryValue>[];
658
+ }),
659
+ );
660
+
661
+ const entries = await Promise.all(collectionEntriesRequests).then(arrays => flatten(arrays));
662
+
663
+ if (errors.length > 0) {
664
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
665
+ // @ts-ignore
666
+ throw new Error({ message: 'Errors occurred while searching entries locally!', errors });
667
+ }
668
+
669
+ const hits = entries
670
+ .filter(({ score }: fuzzy.FilterResult<EntryValue>) => score > 5)
671
+ .sort(sortByScore)
672
+ .map((f: fuzzy.FilterResult<EntryValue>) => f.original);
673
+ return { entries: hits };
674
+ }
675
+
676
+ async query(
677
+ collection: Collection,
678
+ searchFields: string[],
679
+ searchTerm: string,
680
+ file?: string,
681
+ limit?: number,
682
+ ) {
683
+ let entries = await this.listAllEntries(collection);
684
+ if (file) {
685
+ entries = entries.filter(e => e.slug === file);
686
+ }
687
+
688
+ const expandedEntries = expandSearchEntries(entries, searchFields);
689
+
690
+ let hits = fuzzy
691
+ .filter(searchTerm, expandedEntries, {
692
+ extract: entry => {
693
+ return getEntryField(entry.field, entry);
694
+ },
695
+ })
696
+ .sort(sortByScore)
697
+ .map(f => f.original);
698
+
699
+ if (limit !== undefined && limit > 0) {
700
+ hits = hits.slice(0, limit);
701
+ }
702
+
703
+ const merged = mergeExpandedEntries(hits);
704
+ return { query: searchTerm, hits: merged };
705
+ }
706
+
707
+ traverseCursor(cursor: Cursor, action: string) {
708
+ const [data, unwrappedCursor] = cursor.unwrapData();
709
+ // TODO: stop assuming all cursors are for collections
710
+ const collection = data.get('collection') as Collection;
711
+ return this.implementation!.traverseCursor!(unwrappedCursor, action).then(
712
+ async ({ entries, cursor: newCursor }) => ({
713
+ entries: this.processEntries(entries, collection),
714
+ cursor: Cursor.create(newCursor).wrapData({
715
+ cursorType: 'collectionEntries',
716
+ collection,
717
+ }),
718
+ }),
719
+ );
720
+ }
721
+
722
+ async getLocalDraftBackup(collection: Collection, slug: string) {
723
+ const key = getEntryBackupKey(collection.get('name'), slug);
724
+ const backup = await localForage.getItem<BackupEntry>(key);
725
+ if (!backup || !backup.raw.trim()) {
726
+ return {};
727
+ }
728
+ const { raw, path } = backup;
729
+ let { mediaFiles = [] } = backup;
730
+
731
+ mediaFiles = mediaFiles.map(file => {
732
+ // de-serialize the file object
733
+ if (file.file) {
734
+ return { ...file, url: URL.createObjectURL(file.file) };
735
+ }
736
+ return file;
737
+ });
738
+
739
+ const label = selectFileEntryLabel(collection, slug);
740
+
741
+ const formatRawData = (raw: string) => {
742
+ return this.entryWithFormat(collection)(
743
+ createEntry(collection.get('name'), slug, path, {
744
+ raw,
745
+ label,
746
+ mediaFiles,
747
+ meta: { path: prepareMetaPath(path, collection) },
748
+ }),
749
+ );
750
+ };
751
+
752
+ const entry: EntryValue = formatRawData(raw);
753
+ if (hasI18n(collection) && backup.i18n) {
754
+ const i18n = formatI18nBackup(backup.i18n, formatRawData);
755
+ entry.i18n = i18n;
756
+ }
757
+
758
+ return { entry };
759
+ }
760
+
761
+ async persistLocalDraftBackup(entry: EntryMap, collection: Collection) {
762
+ try {
763
+ await this.backupSync.acquire();
764
+ const key = getEntryBackupKey(collection.get('name'), entry.get('slug'));
765
+ const raw = this.entryToRaw(collection, entry);
766
+
767
+ if (!raw.trim()) {
768
+ return;
769
+ }
770
+
771
+ const mediaFiles = await Promise.all<MediaFile>(
772
+ entry
773
+ .get('mediaFiles')
774
+ .toJS()
775
+ .map(async (file: MediaFile) => {
776
+ // make sure to serialize the file
777
+ if (file.url?.startsWith('blob:')) {
778
+ const blob = await fetch(file.url as string).then(res => res.blob());
779
+ return { ...file, file: blobToFileObj(file.name, blob) };
780
+ }
781
+ return file;
782
+ }),
783
+ );
784
+
785
+ let i18n;
786
+ if (hasI18n(collection)) {
787
+ i18n = getI18nBackup(collection, entry, entry => this.entryToRaw(collection, entry));
788
+ }
789
+
790
+ await localForage.setItem<BackupEntry>(key, {
791
+ raw,
792
+ path: entry.get('path'),
793
+ mediaFiles,
794
+ ...(i18n && { i18n }),
795
+ });
796
+ const result = await localForage.setItem(getEntryBackupKey(), raw);
797
+ return result;
798
+ } catch (e) {
799
+ console.warn('persistLocalDraftBackup', e);
800
+ } finally {
801
+ this.backupSync.release();
802
+ }
803
+ }
804
+
805
+ async deleteLocalDraftBackup(collection: Collection, slug: string) {
806
+ try {
807
+ await this.backupSync.acquire();
808
+ await localForage.removeItem(getEntryBackupKey(collection.get('name'), slug));
809
+ // delete new entry backup if not deleted
810
+ slug && (await localForage.removeItem(getEntryBackupKey(collection.get('name'))));
811
+ const result = await this.deleteAnonymousBackup();
812
+ return result;
813
+ } catch (e) {
814
+ console.warn('deleteLocalDraftBackup', e);
815
+ } finally {
816
+ this.backupSync.release();
817
+ }
818
+ }
819
+
820
+ // Unnamed backup for use in the global error boundary, should always be
821
+ // deleted on cms load.
822
+ deleteAnonymousBackup() {
823
+ return localForage.removeItem(getEntryBackupKey());
824
+ }
825
+
826
+ async getEntry(state: State, collection: Collection, slug: string) {
827
+ const path = selectEntryPath(collection, slug) as string;
828
+ const label = selectFileEntryLabel(collection, slug);
829
+ const extension = selectFolderEntryExtension(collection);
830
+
831
+ const getEntryValue = async (path: string) => {
832
+ const loadedEntry = await this.implementation.getEntry(path);
833
+ let entry = createEntry(collection.get('name'), slug, loadedEntry.file.path, {
834
+ raw: loadedEntry.data,
835
+ label,
836
+ mediaFiles: [],
837
+ meta: { path: prepareMetaPath(loadedEntry.file.path, collection) },
838
+ });
839
+
840
+ entry = this.entryWithFormat(collection)(entry);
841
+ entry = await this.processEntry(state, collection, entry);
842
+
843
+ return entry;
844
+ };
845
+
846
+ let entryValue: EntryValue;
847
+ if (hasI18n(collection)) {
848
+ entryValue = await getI18nEntry(collection, extension, path, slug, getEntryValue);
849
+ } else {
850
+ entryValue = await getEntryValue(path);
851
+ }
852
+
853
+ return entryValue;
854
+ }
855
+
856
+ getMedia() {
857
+ return this.implementation.getMedia();
858
+ }
859
+
860
+ getMediaFile(path: string) {
861
+ return this.implementation.getMediaFile(path);
862
+ }
863
+
864
+ getMediaDisplayURL(displayURL: DisplayURL) {
865
+ if (this.implementation.getMediaDisplayURL) {
866
+ return this.implementation.getMediaDisplayURL(displayURL);
867
+ }
868
+ const err = new Error(
869
+ 'getMediaDisplayURL is not implemented by the current backend, but the backend returned a displayURL which was not a string!',
870
+ ) as Error & { displayURL: DisplayURL };
871
+ err.displayURL = displayURL;
872
+ return Promise.reject(err);
873
+ }
874
+
875
+ entryWithFormat(collection: Collection) {
876
+ return (entry: EntryValue): EntryValue => {
877
+ const format = resolveFormat(collection, entry);
878
+ if (entry && entry.raw !== undefined) {
879
+ const data = (format && attempt(format.fromFile.bind(format, entry.raw))) || {};
880
+ if (isError(data)) console.error(data);
881
+ return Object.assign(entry, { data: isError(data) ? {} : data });
882
+ }
883
+ return format.fromFile(entry);
884
+ };
885
+ }
886
+
887
+ async processUnpublishedEntry(
888
+ collection: Collection,
889
+ entryData: UnpublishedEntry,
890
+ withMediaFiles: boolean,
891
+ ) {
892
+ const { slug } = entryData;
893
+ let extension: string;
894
+ if (collection.get('type') === FILES) {
895
+ const file = collection.get('files')!.find(f => f?.get('name') === slug);
896
+ extension = extname(file.get('file'));
897
+ } else {
898
+ extension = selectFolderEntryExtension(collection);
899
+ }
900
+
901
+ const mediaFiles: MediaFile[] = [];
902
+ if (withMediaFiles) {
903
+ const nonDataFiles = entryData.diffs.filter(d => !d.path.endsWith(extension));
904
+ const files = await Promise.all(
905
+ nonDataFiles.map(f =>
906
+ this.implementation!.unpublishedEntryMediaFile(
907
+ collection.get('name'),
908
+ slug,
909
+ f.path,
910
+ f.id,
911
+ ),
912
+ ),
913
+ );
914
+ mediaFiles.push(...files.map(f => ({ ...f, draft: true })));
915
+ }
916
+
917
+ const dataFiles = sortBy(
918
+ entryData.diffs.filter(d => d.path.endsWith(extension)),
919
+ f => f.path.length,
920
+ );
921
+
922
+ const formatData = (data: string, path: string, newFile: boolean) => {
923
+ const entry = createEntry(collection.get('name'), slug, path, {
924
+ raw: data,
925
+ isModification: !newFile,
926
+ label: collection && selectFileEntryLabel(collection, slug),
927
+ mediaFiles,
928
+ updatedOn: entryData.updatedAt,
929
+ author: entryData.pullRequestAuthor,
930
+ status: entryData.status,
931
+ meta: { path: prepareMetaPath(path, collection) },
932
+ });
933
+
934
+ const entryWithFormat = this.entryWithFormat(collection)(entry);
935
+ return entryWithFormat;
936
+ };
937
+
938
+ const readAndFormatDataFile = async (dataFile: UnpublishedEntryDiff) => {
939
+ const data = await this.implementation.unpublishedEntryDataFile(
940
+ collection.get('name'),
941
+ entryData.slug,
942
+ dataFile.path,
943
+ dataFile.id,
944
+ );
945
+ const entryWithFormat = formatData(data, dataFile.path, dataFile.newFile);
946
+ return entryWithFormat;
947
+ };
948
+
949
+ // if the unpublished entry has no diffs, return the original
950
+ if (dataFiles.length <= 0) {
951
+ const loadedEntry = await this.implementation.getEntry(
952
+ selectEntryPath(collection, slug) as string,
953
+ );
954
+ return formatData(loadedEntry.data, loadedEntry.file.path, false);
955
+ } else if (hasI18n(collection)) {
956
+ // we need to read all locales files and not just the changes
957
+ const path = selectEntryPath(collection, slug) as string;
958
+ const i18nFiles = getI18nDataFiles(collection, extension, path, slug, dataFiles);
959
+ let entries = await Promise.all(
960
+ i18nFiles.map(dataFile => readAndFormatDataFile(dataFile).catch(() => null)),
961
+ );
962
+ entries = entries.filter(Boolean);
963
+ const grouped = await groupEntries(collection, extension, entries as EntryValue[]);
964
+ return grouped[0];
965
+ } else {
966
+ const entryWithFormat = await readAndFormatDataFile(dataFiles[0]);
967
+ return entryWithFormat;
968
+ }
969
+ }
970
+
971
+ async unpublishedEntries(collections: Collections) {
972
+ const ids = await this.implementation.unpublishedEntries!();
973
+ const entries = (
974
+ await Promise.all(
975
+ ids.map(async id => {
976
+ const entryData = await this.implementation.unpublishedEntry({ id });
977
+ const collectionName = entryData.collection;
978
+ const collection = collections.find(c => c.get('name') === collectionName);
979
+ if (!collection) {
980
+ console.warn(`Missing collection '${collectionName}' for unpublished entry '${id}'`);
981
+ return null;
982
+ }
983
+ const entry = await this.processUnpublishedEntry(collection, entryData, false);
984
+ return entry;
985
+ }),
986
+ )
987
+ ).filter(Boolean) as EntryValue[];
988
+
989
+ return { pagination: 0, entries };
990
+ }
991
+
992
+ async processEntry(state: State, collection: Collection, entry: EntryValue) {
993
+ const integration = selectIntegration(state.integrations, null, 'assetStore');
994
+ const mediaFolders = selectMediaFolders(state.config, collection, fromJS(entry));
995
+ if (mediaFolders.length > 0 && !integration) {
996
+ const files = await Promise.all(
997
+ mediaFolders.map(folder => this.implementation.getMedia(folder)),
998
+ );
999
+ entry.mediaFiles = entry.mediaFiles.concat(...files);
1000
+ } else {
1001
+ entry.mediaFiles = entry.mediaFiles.concat(state.mediaLibrary.get('files') || []);
1002
+ }
1003
+
1004
+ return entry;
1005
+ }
1006
+
1007
+ async unpublishedEntry(state: State, collection: Collection, slug: string) {
1008
+ const entryData = await this.implementation!.unpublishedEntry!({
1009
+ collection: collection.get('name') as string,
1010
+ slug,
1011
+ });
1012
+
1013
+ let entry = await this.processUnpublishedEntry(collection, entryData, true);
1014
+ entry = await this.processEntry(state, collection, entry);
1015
+ return entry;
1016
+ }
1017
+
1018
+ /**
1019
+ * Creates a URL using `site_url` from the config and `preview_path` from the
1020
+ * entry's collection. Does not currently make a request through the backend,
1021
+ * but likely will in the future.
1022
+ */
1023
+ getDeploy(collection: Collection, slug: string, entry: EntryMap) {
1024
+ /**
1025
+ * If `site_url` is undefined or `show_preview_links` in the config is set to false, do nothing.
1026
+ */
1027
+
1028
+ const baseUrl = this.config.site_url;
1029
+
1030
+ if (!baseUrl || this.config.show_preview_links === false) {
1031
+ return;
1032
+ }
1033
+
1034
+ return {
1035
+ url: previewUrlFormatter(baseUrl, collection, slug, entry, this.config.slug),
1036
+ status: 'SUCCESS',
1037
+ };
1038
+ }
1039
+
1040
+ /**
1041
+ * Requests a base URL from the backend for previewing a specific entry.
1042
+ * Supports polling via `maxAttempts` and `interval` options, as there is
1043
+ * often a delay before a preview URL is available.
1044
+ */
1045
+ async getDeployPreview(
1046
+ collection: Collection,
1047
+ slug: string,
1048
+ entry: EntryMap,
1049
+ { maxAttempts = 1, interval = 5000 } = {},
1050
+ ) {
1051
+ /**
1052
+ * If the registered backend does not provide a `getDeployPreview` method, or
1053
+ * `show_preview_links` in the config is set to false, do nothing.
1054
+ */
1055
+ if (!this.implementation.getDeployPreview || this.config.show_preview_links === false) {
1056
+ return;
1057
+ }
1058
+
1059
+ /**
1060
+ * Poll for the deploy preview URL (defaults to 1 attempt, so no polling by
1061
+ * default).
1062
+ */
1063
+ let deployPreview,
1064
+ count = 0;
1065
+ while (!deployPreview && count < maxAttempts) {
1066
+ count++;
1067
+ deployPreview = await this.implementation.getDeployPreview(collection.get('name'), slug);
1068
+ if (!deployPreview) {
1069
+ await new Promise(resolve => setTimeout(() => resolve(undefined), interval));
1070
+ }
1071
+ }
1072
+
1073
+ /**
1074
+ * If there's no deploy preview, do nothing.
1075
+ */
1076
+ if (!deployPreview) {
1077
+ return;
1078
+ }
1079
+
1080
+ return {
1081
+ /**
1082
+ * Create a URL using the collection `preview_path`, if provided.
1083
+ */
1084
+ url: previewUrlFormatter(deployPreview.url, collection, slug, entry, this.config.slug),
1085
+ /**
1086
+ * Always capitalize the status for consistency.
1087
+ */
1088
+ status: deployPreview.status ? deployPreview.status.toUpperCase() : '',
1089
+ };
1090
+ }
1091
+
1092
+ async persistEntry({
1093
+ config,
1094
+ collection,
1095
+ entryDraft: draft,
1096
+ assetProxies,
1097
+ usedSlugs,
1098
+ unpublished = false,
1099
+ status,
1100
+ }: PersistArgs) {
1101
+ const updatedEntity = await this.invokePreSaveEvent(draft.get('entry'));
1102
+
1103
+ let entryDraft;
1104
+ if (updatedEntity.get('data') === undefined) {
1105
+ entryDraft = (updatedEntity && draft.setIn(['entry', 'data'], updatedEntity)) || draft;
1106
+ } else {
1107
+ entryDraft = (updatedEntity && draft.setIn(['entry'], updatedEntity)) || draft;
1108
+ }
1109
+
1110
+ const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false;
1111
+
1112
+ const useWorkflow = selectUseWorkflow(config);
1113
+
1114
+ const customPath = selectCustomPath(collection, entryDraft);
1115
+
1116
+ let dataFile: DataFile;
1117
+ if (newEntry) {
1118
+ if (!selectAllowNewEntries(collection)) {
1119
+ throw new Error('Not allowed to create new entries in this collection');
1120
+ }
1121
+ const slug = await this.generateUniqueSlug(
1122
+ collection,
1123
+ entryDraft.getIn(['entry', 'data']),
1124
+ config,
1125
+ usedSlugs,
1126
+ customPath,
1127
+ );
1128
+ const path = customPath || (selectEntryPath(collection, slug) as string);
1129
+ dataFile = {
1130
+ path,
1131
+ slug,
1132
+ raw: this.entryToRaw(collection, entryDraft.get('entry')),
1133
+ };
1134
+
1135
+ updateAssetProxies(assetProxies, config, collection, entryDraft, path);
1136
+ } else {
1137
+ const slug = entryDraft.getIn(['entry', 'slug']);
1138
+ const path = entryDraft.getIn(['entry', 'path']);
1139
+ dataFile = {
1140
+ path,
1141
+ // for workflow entries we refresh the slug on publish
1142
+ slug: customPath && !useWorkflow ? slugFromCustomPath(collection, customPath) : slug,
1143
+ raw: this.entryToRaw(collection, entryDraft.get('entry')),
1144
+ newPath: customPath === path ? undefined : customPath,
1145
+ };
1146
+ }
1147
+
1148
+ const { slug, path, newPath } = dataFile;
1149
+
1150
+ let dataFiles = [dataFile];
1151
+ if (hasI18n(collection)) {
1152
+ const extension = selectFolderEntryExtension(collection);
1153
+ dataFiles = getI18nFiles(
1154
+ collection,
1155
+ extension,
1156
+ entryDraft.get('entry'),
1157
+ (draftData: EntryMap) => this.entryToRaw(collection, draftData),
1158
+ path,
1159
+ slug,
1160
+ newPath,
1161
+ );
1162
+ }
1163
+
1164
+ const user = (await this.currentUser()) as User;
1165
+ const commitMessage = commitMessageFormatter(
1166
+ newEntry ? 'create' : 'update',
1167
+ config,
1168
+ {
1169
+ collection,
1170
+ slug,
1171
+ path,
1172
+ authorLogin: user.login,
1173
+ authorName: user.name,
1174
+ },
1175
+ user.useOpenAuthoring,
1176
+ );
1177
+
1178
+ const collectionName = collection.get('name');
1179
+ const hasSubfolders = collection.get('nested')?.get('subfolders') !== false;
1180
+
1181
+ const updatedOptions = { unpublished, status };
1182
+ const opts = {
1183
+ newEntry,
1184
+ commitMessage,
1185
+ collectionName,
1186
+ useWorkflow,
1187
+ hasSubfolders,
1188
+ ...updatedOptions,
1189
+ };
1190
+
1191
+ if (!useWorkflow) {
1192
+ await this.invokePrePublishEvent(entryDraft.get('entry'));
1193
+ }
1194
+
1195
+ await this.implementation.persistEntry(
1196
+ {
1197
+ dataFiles,
1198
+ assets: assetProxies,
1199
+ },
1200
+ opts,
1201
+ );
1202
+
1203
+ await this.invokePostSaveEvent(entryDraft.get('entry'));
1204
+
1205
+ if (!useWorkflow) {
1206
+ await this.invokePostPublishEvent(entryDraft.get('entry'));
1207
+ }
1208
+
1209
+ return slug;
1210
+ }
1211
+
1212
+ async invokeEventWithEntry(event: string, entry: EntryMap) {
1213
+ const { login, name } = (await this.currentUser()) as User;
1214
+ return await invokeEvent({ name: event, data: { entry, author: { login, name } } });
1215
+ }
1216
+
1217
+ async invokePrePublishEvent(entry: EntryMap) {
1218
+ await this.invokeEventWithEntry('prePublish', entry);
1219
+ }
1220
+
1221
+ async invokePostPublishEvent(entry: EntryMap) {
1222
+ await this.invokeEventWithEntry('postPublish', entry);
1223
+ }
1224
+
1225
+ async invokePreUnpublishEvent(entry: EntryMap) {
1226
+ await this.invokeEventWithEntry('preUnpublish', entry);
1227
+ }
1228
+
1229
+ async invokePostUnpublishEvent(entry: EntryMap) {
1230
+ await this.invokeEventWithEntry('postUnpublish', entry);
1231
+ }
1232
+
1233
+ async invokePreSaveEvent(entry: EntryMap) {
1234
+ return await this.invokeEventWithEntry('preSave', entry);
1235
+ }
1236
+
1237
+ async invokePostSaveEvent(entry: EntryMap) {
1238
+ await this.invokeEventWithEntry('postSave', entry);
1239
+ }
1240
+
1241
+ async persistMedia(config: CmsConfig, file: AssetProxy) {
1242
+ const user = (await this.currentUser()) as User;
1243
+ const options = {
1244
+ commitMessage: commitMessageFormatter(
1245
+ 'uploadMedia',
1246
+ config,
1247
+ {
1248
+ path: file.path,
1249
+ authorLogin: user.login,
1250
+ authorName: user.name,
1251
+ },
1252
+ user.useOpenAuthoring,
1253
+ ),
1254
+ };
1255
+ return this.implementation.persistMedia(file, options);
1256
+ }
1257
+
1258
+ async deleteEntry(state: State, collection: Collection, slug: string) {
1259
+ const config = state.config;
1260
+ const path = selectEntryPath(collection, slug) as string;
1261
+ const extension = selectFolderEntryExtension(collection) as string;
1262
+
1263
+ if (!selectAllowDeletion(collection)) {
1264
+ throw new Error('Not allowed to delete entries in this collection');
1265
+ }
1266
+
1267
+ const user = (await this.currentUser()) as User;
1268
+ const commitMessage = commitMessageFormatter(
1269
+ 'delete',
1270
+ config,
1271
+ {
1272
+ collection,
1273
+ slug,
1274
+ path,
1275
+ authorLogin: user.login,
1276
+ authorName: user.name,
1277
+ },
1278
+ user.useOpenAuthoring,
1279
+ );
1280
+
1281
+ const entry = selectEntry(state.entries, collection.get('name'), slug);
1282
+ await this.invokePreUnpublishEvent(entry);
1283
+ let paths = [path];
1284
+ if (hasI18n(collection)) {
1285
+ paths = getFilePaths(collection, extension, path, slug);
1286
+ }
1287
+ await this.implementation.deleteFiles(paths, commitMessage);
1288
+
1289
+ await this.invokePostUnpublishEvent(entry);
1290
+ }
1291
+
1292
+ async deleteMedia(config: CmsConfig, path: string) {
1293
+ const user = (await this.currentUser()) as User;
1294
+ const commitMessage = commitMessageFormatter(
1295
+ 'deleteMedia',
1296
+ config,
1297
+ {
1298
+ path,
1299
+ authorLogin: user.login,
1300
+ authorName: user.name,
1301
+ },
1302
+ user.useOpenAuthoring,
1303
+ );
1304
+ return this.implementation.deleteFiles([path], commitMessage);
1305
+ }
1306
+
1307
+ persistUnpublishedEntry(args: PersistArgs) {
1308
+ return this.persistEntry({ ...args, unpublished: true });
1309
+ }
1310
+
1311
+ updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
1312
+ return this.implementation.updateUnpublishedEntryStatus!(collection, slug, newStatus);
1313
+ }
1314
+
1315
+ async publishUnpublishedEntry(entry: EntryMap) {
1316
+ const collection = entry.get('collection');
1317
+ const slug = entry.get('slug');
1318
+
1319
+ await this.invokePrePublishEvent(entry);
1320
+ await this.implementation.publishUnpublishedEntry!(collection, slug);
1321
+ await this.invokePostPublishEvent(entry);
1322
+ }
1323
+
1324
+ deleteUnpublishedEntry(collection: string, slug: string) {
1325
+ return this.implementation.deleteUnpublishedEntry!(collection, slug);
1326
+ }
1327
+
1328
+ entryToRaw(collection: Collection, entry: EntryMap): string {
1329
+ const format = resolveFormat(collection, entry.toJS());
1330
+ const fieldsOrder = this.fieldsOrder(collection, entry);
1331
+ const fieldsComments = selectFieldsComments(collection, entry);
1332
+ let content = format.toFile(entry.get('data').toJS(), fieldsOrder, fieldsComments);
1333
+ if (content.slice(-1) != '\n') {
1334
+ // add the EOL if it does not exist.
1335
+ content += '\n';
1336
+ }
1337
+ return content;
1338
+ }
1339
+
1340
+ fieldsOrder(collection: Collection, entry: EntryMap) {
1341
+ const fields = collection.get('fields');
1342
+ if (fields) {
1343
+ return collection
1344
+ .get('fields')
1345
+ .map(f => f!.get('name'))
1346
+ .toArray();
1347
+ }
1348
+
1349
+ const files = collection.get('files');
1350
+ const file = (files || List<CollectionFile>())
1351
+ .filter(f => f!.get('name') === entry.get('slug'))
1352
+ .get(0);
1353
+
1354
+ if (file == null) {
1355
+ throw new Error(`No file found for ${entry.get('slug')} in ${collection.get('name')}`);
1356
+ }
1357
+ return file
1358
+ .get('fields')
1359
+ .map(f => f!.get('name'))
1360
+ .toArray();
1361
+ }
1362
+
1363
+ filterEntries(collection: { entries: EntryValue[] }, filterRule: FilterRule) {
1364
+ return collection.entries.filter(entry => {
1365
+ const fieldValue = entry.data[filterRule.get('field')];
1366
+ if (Array.isArray(fieldValue)) {
1367
+ return fieldValue.includes(filterRule.get('value'));
1368
+ }
1369
+ return fieldValue === filterRule.get('value');
1370
+ });
1371
+ }
1372
+ }
1373
+
1374
+ export function resolveBackend(config: CmsConfig) {
1375
+ if (!config.backend.name) {
1376
+ throw new Error('No backend defined in configuration');
1377
+ }
1378
+
1379
+ const { name } = config.backend;
1380
+ const authStore = new LocalStorageAuthStore();
1381
+
1382
+ const backend = getBackend(name);
1383
+ if (!backend) {
1384
+ throw new Error(`Backend not found: ${name}`);
1385
+ } else {
1386
+ return new Backend(backend, { backendName: name, authStore, config });
1387
+ }
1388
+ }
1389
+
1390
+ export const currentBackend = (function () {
1391
+ let backend: Backend;
1392
+
1393
+ return (config: CmsConfig) => {
1394
+ if (backend) {
1395
+ return backend;
1396
+ }
1397
+
1398
+ return (backend = resolveBackend(config));
1399
+ };
1400
+ })();