@strapi/content-manager 0.0.0-experimental.a65a85fdea97faae8679d3ffc5f9d79af61abd26 → 0.0.0-experimental.a6728ad43ac70ae19dabb624dbfca1f2d9610a86

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 (189) hide show
  1. package/LICENSE +18 -3
  2. package/dist/_chunks/{CardDragPreview-DSVYodBX.js → CardDragPreview-C0QyJgRA.js} +10 -14
  3. package/dist/_chunks/CardDragPreview-C0QyJgRA.js.map +1 -0
  4. package/dist/_chunks/{CardDragPreview-ikSG4M46.mjs → CardDragPreview-DOxamsuj.mjs} +7 -9
  5. package/dist/_chunks/CardDragPreview-DOxamsuj.mjs.map +1 -0
  6. package/dist/_chunks/{ComponentConfigurationPage--2aLCv-G.mjs → ComponentConfigurationPage-DJ5voqEK.mjs} +3 -3
  7. package/dist/_chunks/{ComponentConfigurationPage--2aLCv-G.mjs.map → ComponentConfigurationPage-DJ5voqEK.mjs.map} +1 -1
  8. package/dist/_chunks/{ComponentConfigurationPage-43KmCNQE.js → ComponentConfigurationPage-_6osrv39.js} +3 -3
  9. package/dist/_chunks/{ComponentConfigurationPage-43KmCNQE.js.map → ComponentConfigurationPage-_6osrv39.js.map} +1 -1
  10. package/dist/_chunks/{ComponentIcon-BBQsYCVn.js → ComponentIcon-BXdiCGQp.js} +8 -2
  11. package/dist/_chunks/ComponentIcon-BXdiCGQp.js.map +1 -0
  12. package/dist/_chunks/{ComponentIcon-BOFnK76n.mjs → ComponentIcon-u4bIXTFY.mjs} +9 -3
  13. package/dist/_chunks/ComponentIcon-u4bIXTFY.mjs.map +1 -0
  14. package/dist/_chunks/{EditConfigurationPage-CUcGHHvQ.mjs → EditConfigurationPage-CZofxSLy.mjs} +3 -3
  15. package/dist/_chunks/{EditConfigurationPage-CUcGHHvQ.mjs.map → EditConfigurationPage-CZofxSLy.mjs.map} +1 -1
  16. package/dist/_chunks/{EditConfigurationPage-BfFzJ4Br.js → EditConfigurationPage-ZN3s568V.js} +3 -3
  17. package/dist/_chunks/{EditConfigurationPage-BfFzJ4Br.js.map → EditConfigurationPage-ZN3s568V.js.map} +1 -1
  18. package/dist/_chunks/{EditViewPage-CzOT5Kpj.js → EditViewPage-Co2IKQZH.js} +58 -49
  19. package/dist/_chunks/EditViewPage-Co2IKQZH.js.map +1 -0
  20. package/dist/_chunks/{EditViewPage-Bm8lgcm6.mjs → EditViewPage-HYljoEY7.mjs} +59 -48
  21. package/dist/_chunks/EditViewPage-HYljoEY7.mjs.map +1 -0
  22. package/dist/_chunks/{Field-Dlh0uGnL.mjs → Field-BOPUMZ1u.mjs} +995 -795
  23. package/dist/_chunks/Field-BOPUMZ1u.mjs.map +1 -0
  24. package/dist/_chunks/{Field-Caef4JjM.js → Field-G9CkFUtP.js} +1041 -842
  25. package/dist/_chunks/Field-G9CkFUtP.js.map +1 -0
  26. package/dist/_chunks/{Form-EnaQL_6L.mjs → Form-CDwNp7pU.mjs} +69 -48
  27. package/dist/_chunks/Form-CDwNp7pU.mjs.map +1 -0
  28. package/dist/_chunks/{Form-BzuAjtRq.js → Form-crsbkGxI.js} +68 -48
  29. package/dist/_chunks/Form-crsbkGxI.js.map +1 -0
  30. package/dist/_chunks/{History-D6sbCJvo.mjs → History-BDZrgfZ3.mjs} +151 -57
  31. package/dist/_chunks/History-BDZrgfZ3.mjs.map +1 -0
  32. package/dist/_chunks/{History-C17LiyRg.js → History-CWcM9HnW.js} +151 -58
  33. package/dist/_chunks/History-CWcM9HnW.js.map +1 -0
  34. package/dist/_chunks/{ListConfigurationPage-Ce4qs7qE.mjs → ListConfigurationPage-BZ3ScUna.mjs} +67 -57
  35. package/dist/_chunks/ListConfigurationPage-BZ3ScUna.mjs.map +1 -0
  36. package/dist/_chunks/{ListConfigurationPage-Dks5SX6f.js → ListConfigurationPage-DGzoQD_I.js} +70 -61
  37. package/dist/_chunks/ListConfigurationPage-DGzoQD_I.js.map +1 -0
  38. package/dist/_chunks/{ListViewPage-BwrZrPsh.js → ListViewPage-BBAC9aPu.js} +132 -139
  39. package/dist/_chunks/ListViewPage-BBAC9aPu.js.map +1 -0
  40. package/dist/_chunks/{ListViewPage-Be7S5aKL.mjs → ListViewPage-CsX7tWx-.mjs} +129 -136
  41. package/dist/_chunks/ListViewPage-CsX7tWx-.mjs.map +1 -0
  42. package/dist/_chunks/{NoContentTypePage-Cu5r1-JT.js → NoContentTypePage-CwVDx_YC.js} +5 -5
  43. package/dist/_chunks/NoContentTypePage-CwVDx_YC.js.map +1 -0
  44. package/dist/_chunks/{NoContentTypePage-CIPmYQMm.mjs → NoContentTypePage-LClTUPWs.mjs} +7 -7
  45. package/dist/_chunks/NoContentTypePage-LClTUPWs.mjs.map +1 -0
  46. package/dist/_chunks/{NoPermissionsPage-C-j6TEUF.js → NoPermissionsPage-D2iWw-sn.js} +4 -5
  47. package/dist/_chunks/NoPermissionsPage-D2iWw-sn.js.map +1 -0
  48. package/dist/_chunks/{NoPermissionsPage-DhJ7LYrr.mjs → NoPermissionsPage-S4Re3FwO.mjs} +5 -6
  49. package/dist/_chunks/NoPermissionsPage-S4Re3FwO.mjs.map +1 -0
  50. package/dist/_chunks/{Relations-CY7AtkDA.mjs → Relations-Dmv0Tpe5.mjs} +67 -57
  51. package/dist/_chunks/Relations-Dmv0Tpe5.mjs.map +1 -0
  52. package/dist/_chunks/{Relations-Czs-uZ-s.js → Relations-jwuTFGOV.js} +71 -62
  53. package/dist/_chunks/Relations-jwuTFGOV.js.map +1 -0
  54. package/dist/_chunks/{en-C-V1_90f.js → en-BlhnxQfj.js} +17 -9
  55. package/dist/_chunks/{en-C-V1_90f.js.map → en-BlhnxQfj.js.map} +1 -1
  56. package/dist/_chunks/{en-MBPul9Su.mjs → en-C8YBvRrK.mjs} +17 -9
  57. package/dist/_chunks/{en-MBPul9Su.mjs.map → en-C8YBvRrK.mjs.map} +1 -1
  58. package/dist/_chunks/{index-DNVx8ssZ.mjs → index-BmUAydCA.mjs} +1715 -813
  59. package/dist/_chunks/index-BmUAydCA.mjs.map +1 -0
  60. package/dist/_chunks/{index-X_2tafck.js → index-CBX6KyXv.js} +1815 -914
  61. package/dist/_chunks/index-CBX6KyXv.js.map +1 -0
  62. package/dist/_chunks/{layout-Dnh0PNp9.mjs → layout-ClP-DC72.mjs} +47 -29
  63. package/dist/_chunks/layout-ClP-DC72.mjs.map +1 -0
  64. package/dist/_chunks/{layout-dBc7wN7L.js → layout-CxxkX9jY.js} +47 -31
  65. package/dist/_chunks/layout-CxxkX9jY.js.map +1 -0
  66. package/dist/_chunks/{relations-4pHtBrHJ.js → relations-DIjTADIu.js} +2 -2
  67. package/dist/_chunks/{relations-4pHtBrHJ.js.map → relations-DIjTADIu.js.map} +1 -1
  68. package/dist/_chunks/{relations-Dx7tMKJN.mjs → relations-op89RClB.mjs} +2 -2
  69. package/dist/_chunks/{relations-Dx7tMKJN.mjs.map → relations-op89RClB.mjs.map} +1 -1
  70. package/dist/_chunks/useDebounce-CtcjDB3L.js +28 -0
  71. package/dist/_chunks/useDebounce-CtcjDB3L.js.map +1 -0
  72. package/dist/_chunks/useDebounce-DmuSJIF3.mjs +29 -0
  73. package/dist/_chunks/useDebounce-DmuSJIF3.mjs.map +1 -0
  74. package/dist/_chunks/useDragAndDrop-DdHgKsqq.mjs.map +1 -1
  75. package/dist/_chunks/useDragAndDrop-J0TUUbR6.js.map +1 -1
  76. package/dist/admin/index.js +3 -1
  77. package/dist/admin/index.js.map +1 -1
  78. package/dist/admin/index.mjs +9 -7
  79. package/dist/admin/src/components/ComponentIcon.d.ts +6 -3
  80. package/dist/admin/src/content-manager.d.ts +3 -3
  81. package/dist/admin/src/exports.d.ts +2 -1
  82. package/dist/admin/src/history/components/VersionInputRenderer.d.ts +1 -1
  83. package/dist/admin/src/history/index.d.ts +3 -0
  84. package/dist/admin/src/history/services/historyVersion.d.ts +1 -1
  85. package/dist/admin/src/hooks/useDocument.d.ts +35 -9
  86. package/dist/admin/src/hooks/useDocumentActions.d.ts +24 -3
  87. package/dist/admin/src/hooks/useDocumentLayout.d.ts +2 -2
  88. package/dist/admin/src/hooks/useDragAndDrop.d.ts +4 -4
  89. package/dist/admin/src/hooks/useKeyboardDragAndDrop.d.ts +1 -1
  90. package/dist/admin/src/index.d.ts +1 -0
  91. package/dist/admin/src/pages/EditView/components/DocumentActions.d.ts +11 -4
  92. package/dist/admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksInput.d.ts +3 -3
  93. package/dist/admin/src/pages/EditView/components/FormInputs/BlocksInput/utils/constants.d.ts +4 -0
  94. package/dist/admin/src/pages/EditView/components/FormInputs/Component/Input.d.ts +2 -2
  95. package/dist/admin/src/pages/EditView/components/FormInputs/DynamicZone/ComponentCategory.d.ts +3 -5
  96. package/dist/admin/src/pages/EditView/components/FormInputs/DynamicZone/Field.d.ts +1 -1
  97. package/dist/admin/src/pages/EditView/components/FormInputs/Relations.d.ts +30 -18
  98. package/dist/admin/src/pages/EditView/components/FormInputs/UID.d.ts +2 -2
  99. package/dist/admin/src/pages/EditView/components/FormInputs/Wysiwyg/EditorLayout.d.ts +3 -49
  100. package/dist/admin/src/pages/EditView/components/FormInputs/Wysiwyg/Field.d.ts +2 -2
  101. package/dist/admin/src/pages/EditView/components/FormInputs/Wysiwyg/WysiwygFooter.d.ts +2 -2
  102. package/dist/admin/src/pages/EditView/components/FormInputs/Wysiwyg/WysiwygStyles.d.ts +16 -53
  103. package/dist/admin/src/pages/EditView/components/Header.d.ts +10 -11
  104. package/dist/admin/src/pages/EditView/components/InputRenderer.d.ts +2 -10
  105. package/dist/admin/src/pages/ListView/components/BulkActions/Actions.d.ts +3 -30
  106. package/dist/admin/src/pages/ListView/components/BulkActions/ConfirmBulkActionDialog.d.ts +2 -2
  107. package/dist/admin/src/pages/ListView/components/BulkActions/PublishAction.d.ts +9 -26
  108. package/dist/admin/src/services/api.d.ts +2 -3
  109. package/dist/admin/src/services/components.d.ts +2 -2
  110. package/dist/admin/src/services/contentTypes.d.ts +5 -5
  111. package/dist/admin/src/services/documents.d.ts +31 -17
  112. package/dist/admin/src/services/init.d.ts +2 -2
  113. package/dist/admin/src/services/relations.d.ts +3 -3
  114. package/dist/admin/src/services/uid.d.ts +3 -3
  115. package/dist/admin/src/utils/api.d.ts +4 -18
  116. package/dist/admin/src/utils/validation.d.ts +5 -7
  117. package/dist/server/index.js +648 -447
  118. package/dist/server/index.js.map +1 -1
  119. package/dist/server/index.mjs +656 -455
  120. package/dist/server/index.mjs.map +1 -1
  121. package/dist/server/src/controllers/collection-types.d.ts.map +1 -1
  122. package/dist/server/src/controllers/relations.d.ts.map +1 -1
  123. package/dist/server/src/controllers/single-types.d.ts.map +1 -1
  124. package/dist/server/src/controllers/uid.d.ts.map +1 -1
  125. package/dist/server/src/controllers/utils/metadata.d.ts +8 -0
  126. package/dist/server/src/controllers/utils/metadata.d.ts.map +1 -0
  127. package/dist/server/src/controllers/validation/dimensions.d.ts +11 -0
  128. package/dist/server/src/controllers/validation/dimensions.d.ts.map +1 -0
  129. package/dist/server/src/controllers/validation/index.d.ts +1 -1
  130. package/dist/server/src/history/services/history.d.ts +2 -4
  131. package/dist/server/src/history/services/history.d.ts.map +1 -1
  132. package/dist/server/src/history/services/index.d.ts +6 -2
  133. package/dist/server/src/history/services/index.d.ts.map +1 -1
  134. package/dist/server/src/history/services/lifecycles.d.ts +9 -0
  135. package/dist/server/src/history/services/lifecycles.d.ts.map +1 -0
  136. package/dist/server/src/history/services/utils.d.ts +42 -9
  137. package/dist/server/src/history/services/utils.d.ts.map +1 -1
  138. package/dist/server/src/history/utils.d.ts +6 -2
  139. package/dist/server/src/history/utils.d.ts.map +1 -1
  140. package/dist/server/src/index.d.ts +18 -39
  141. package/dist/server/src/index.d.ts.map +1 -1
  142. package/dist/server/src/policies/hasPermissions.d.ts.map +1 -1
  143. package/dist/server/src/services/document-manager.d.ts +13 -12
  144. package/dist/server/src/services/document-manager.d.ts.map +1 -1
  145. package/dist/server/src/services/document-metadata.d.ts +8 -29
  146. package/dist/server/src/services/document-metadata.d.ts.map +1 -1
  147. package/dist/server/src/services/index.d.ts +18 -39
  148. package/dist/server/src/services/index.d.ts.map +1 -1
  149. package/dist/server/src/services/permission-checker.d.ts.map +1 -1
  150. package/dist/server/src/services/utils/populate.d.ts +8 -1
  151. package/dist/server/src/services/utils/populate.d.ts.map +1 -1
  152. package/dist/shared/contracts/collection-types.d.ts +17 -7
  153. package/dist/shared/contracts/collection-types.d.ts.map +1 -1
  154. package/dist/shared/contracts/relations.d.ts +2 -2
  155. package/dist/shared/contracts/relations.d.ts.map +1 -1
  156. package/package.json +16 -17
  157. package/dist/_chunks/CardDragPreview-DSVYodBX.js.map +0 -1
  158. package/dist/_chunks/CardDragPreview-ikSG4M46.mjs.map +0 -1
  159. package/dist/_chunks/ComponentIcon-BBQsYCVn.js.map +0 -1
  160. package/dist/_chunks/ComponentIcon-BOFnK76n.mjs.map +0 -1
  161. package/dist/_chunks/EditViewPage-Bm8lgcm6.mjs.map +0 -1
  162. package/dist/_chunks/EditViewPage-CzOT5Kpj.js.map +0 -1
  163. package/dist/_chunks/Field-Caef4JjM.js.map +0 -1
  164. package/dist/_chunks/Field-Dlh0uGnL.mjs.map +0 -1
  165. package/dist/_chunks/Form-BzuAjtRq.js.map +0 -1
  166. package/dist/_chunks/Form-EnaQL_6L.mjs.map +0 -1
  167. package/dist/_chunks/History-C17LiyRg.js.map +0 -1
  168. package/dist/_chunks/History-D6sbCJvo.mjs.map +0 -1
  169. package/dist/_chunks/ListConfigurationPage-Ce4qs7qE.mjs.map +0 -1
  170. package/dist/_chunks/ListConfigurationPage-Dks5SX6f.js.map +0 -1
  171. package/dist/_chunks/ListViewPage-Be7S5aKL.mjs.map +0 -1
  172. package/dist/_chunks/ListViewPage-BwrZrPsh.js.map +0 -1
  173. package/dist/_chunks/NoContentTypePage-CIPmYQMm.mjs.map +0 -1
  174. package/dist/_chunks/NoContentTypePage-Cu5r1-JT.js.map +0 -1
  175. package/dist/_chunks/NoPermissionsPage-C-j6TEUF.js.map +0 -1
  176. package/dist/_chunks/NoPermissionsPage-DhJ7LYrr.mjs.map +0 -1
  177. package/dist/_chunks/Relations-CY7AtkDA.mjs.map +0 -1
  178. package/dist/_chunks/Relations-Czs-uZ-s.js.map +0 -1
  179. package/dist/_chunks/index-DNVx8ssZ.mjs.map +0 -1
  180. package/dist/_chunks/index-X_2tafck.js.map +0 -1
  181. package/dist/_chunks/layout-Dnh0PNp9.mjs.map +0 -1
  182. package/dist/_chunks/layout-dBc7wN7L.js.map +0 -1
  183. package/dist/_chunks/urls-CbOsUOoW.mjs +0 -7
  184. package/dist/_chunks/urls-CbOsUOoW.mjs.map +0 -1
  185. package/dist/_chunks/urls-DzZya_gm.js +0 -6
  186. package/dist/_chunks/urls-DzZya_gm.js.map +0 -1
  187. package/dist/server/src/controllers/utils/dimensions.d.ts +0 -5
  188. package/dist/server/src/controllers/utils/dimensions.d.ts.map +0 -1
  189. package/strapi-server.js +0 -3
@@ -138,43 +138,70 @@ const FIELDS_TO_IGNORE = [
138
138
  "strapi_stage",
139
139
  "strapi_assignee"
140
140
  ];
141
- const getSchemaAttributesDiff = (versionSchemaAttributes, contentTypeSchemaAttributes) => {
142
- const sanitizedContentTypeSchemaAttributes = fp.omit(FIELDS_TO_IGNORE, contentTypeSchemaAttributes);
143
- const reduceDifferenceToAttributesObject = (diffKeys, source) => {
144
- return diffKeys.reduce((previousAttributesObject, diffKey) => {
145
- previousAttributesObject[diffKey] = source[diffKey];
146
- return previousAttributesObject;
147
- }, {});
148
- };
149
- const versionSchemaKeys = Object.keys(versionSchemaAttributes);
150
- const contentTypeSchemaAttributesKeys = Object.keys(sanitizedContentTypeSchemaAttributes);
151
- const uniqueToContentType = fp.difference(contentTypeSchemaAttributesKeys, versionSchemaKeys);
152
- const added = reduceDifferenceToAttributesObject(
153
- uniqueToContentType,
154
- sanitizedContentTypeSchemaAttributes
155
- );
156
- const uniqueToVersion = fp.difference(versionSchemaKeys, contentTypeSchemaAttributesKeys);
157
- const removed = reduceDifferenceToAttributesObject(uniqueToVersion, versionSchemaAttributes);
158
- return { added, removed };
159
- };
160
141
  const DEFAULT_RETENTION_DAYS = 90;
161
- const createHistoryService = ({ strapi: strapi2 }) => {
162
- const state = {
163
- deleteExpiredJob: null,
164
- isInitialized: false
142
+ const createServiceUtils = ({ strapi: strapi2 }) => {
143
+ const getSchemaAttributesDiff = (versionSchemaAttributes, contentTypeSchemaAttributes) => {
144
+ const sanitizedContentTypeSchemaAttributes = fp.omit(
145
+ FIELDS_TO_IGNORE,
146
+ contentTypeSchemaAttributes
147
+ );
148
+ const reduceDifferenceToAttributesObject = (diffKeys, source) => {
149
+ return diffKeys.reduce(
150
+ (previousAttributesObject, diffKey) => {
151
+ previousAttributesObject[diffKey] = source[diffKey];
152
+ return previousAttributesObject;
153
+ },
154
+ {}
155
+ );
156
+ };
157
+ const versionSchemaKeys = Object.keys(versionSchemaAttributes);
158
+ const contentTypeSchemaAttributesKeys = Object.keys(sanitizedContentTypeSchemaAttributes);
159
+ const uniqueToContentType = fp.difference(contentTypeSchemaAttributesKeys, versionSchemaKeys);
160
+ const added = reduceDifferenceToAttributesObject(
161
+ uniqueToContentType,
162
+ sanitizedContentTypeSchemaAttributes
163
+ );
164
+ const uniqueToVersion = fp.difference(versionSchemaKeys, contentTypeSchemaAttributesKeys);
165
+ const removed = reduceDifferenceToAttributesObject(uniqueToVersion, versionSchemaAttributes);
166
+ return { added, removed };
165
167
  };
166
- const query = strapi2.db.query(HISTORY_VERSION_UID);
167
- const getRetentionDays = (strapi22) => {
168
- const featureConfig = strapi22.ee.features.get("cms-content-history");
169
- const licenseRetentionDays = typeof featureConfig === "object" && featureConfig?.options.retentionDays;
170
- const userRetentionDays = strapi22.config.get("admin.history.retentionDays");
171
- if (userRetentionDays && userRetentionDays < licenseRetentionDays) {
172
- return userRetentionDays;
168
+ const getRelationRestoreValue = async (versionRelationData, attribute) => {
169
+ if (Array.isArray(versionRelationData)) {
170
+ if (versionRelationData.length === 0)
171
+ return versionRelationData;
172
+ const existingAndMissingRelations = await Promise.all(
173
+ versionRelationData.map((relation) => {
174
+ return strapi2.documents(attribute.target).findOne({
175
+ documentId: relation.documentId,
176
+ locale: relation.locale || void 0
177
+ });
178
+ })
179
+ );
180
+ return existingAndMissingRelations.filter(
181
+ (relation) => relation !== null
182
+ );
173
183
  }
174
- return Math.min(licenseRetentionDays, DEFAULT_RETENTION_DAYS);
184
+ return strapi2.documents(attribute.target).findOne({
185
+ documentId: versionRelationData.documentId,
186
+ locale: versionRelationData.locale || void 0
187
+ });
188
+ };
189
+ const getMediaRestoreValue = async (versionRelationData, attribute) => {
190
+ if (attribute.multiple) {
191
+ const existingAndMissingMedias = await Promise.all(
192
+ // @ts-expect-error Fix the type definitions so this isn't any
193
+ versionRelationData.map((media) => {
194
+ return strapi2.db.query("plugin::upload.file").findOne({ where: { id: media.id } });
195
+ })
196
+ );
197
+ return existingAndMissingMedias.filter((media) => media != null);
198
+ }
199
+ return strapi2.db.query("plugin::upload.file").findOne({ where: { id: versionRelationData.id } });
175
200
  };
176
201
  const localesService = strapi2.plugin("i18n")?.service("locales");
202
+ const i18nContentTypeService = strapi2.plugin("i18n")?.service("content-types");
177
203
  const getDefaultLocale = async () => localesService ? localesService.getDefaultLocale() : null;
204
+ const isLocalizedContentType = (model) => i18nContentTypeService ? i18nContentTypeService.isLocalizedContentType(model) : false;
178
205
  const getLocaleDictionary = async () => {
179
206
  if (!localesService)
180
207
  return {};
@@ -187,25 +214,39 @@ const createHistoryService = ({ strapi: strapi2 }) => {
187
214
  {}
188
215
  );
189
216
  };
217
+ const getRetentionDays = () => {
218
+ const featureConfig = strapi2.ee.features.get("cms-content-history");
219
+ const licenseRetentionDays = typeof featureConfig === "object" && featureConfig?.options.retentionDays;
220
+ const userRetentionDays = strapi2.config.get("admin.history.retentionDays");
221
+ if (userRetentionDays && userRetentionDays < licenseRetentionDays) {
222
+ return userRetentionDays;
223
+ }
224
+ return Math.min(licenseRetentionDays, DEFAULT_RETENTION_DAYS);
225
+ };
190
226
  const getVersionStatus = async (contentTypeUid, document) => {
191
227
  const documentMetadataService = strapi2.plugin("content-manager").service("document-metadata");
192
228
  const meta = await documentMetadataService.getMetadata(contentTypeUid, document);
193
229
  return documentMetadataService.getStatus(document, meta.availableStatus);
194
230
  };
195
- const getDeepPopulate2 = (uid2) => {
231
+ const getDeepPopulate2 = (uid2, useDatabaseSyntax = false) => {
196
232
  const model = strapi2.getModel(uid2);
197
233
  const attributes = Object.entries(model.attributes);
234
+ const fieldSelector = useDatabaseSyntax ? "select" : "fields";
198
235
  return attributes.reduce((acc, [attributeName, attribute]) => {
199
236
  switch (attribute.type) {
200
237
  case "relation": {
238
+ const isMorphRelation = attribute.relation.toLowerCase().startsWith("morph");
239
+ if (isMorphRelation) {
240
+ break;
241
+ }
201
242
  const isVisible2 = strapiUtils.contentTypes.isVisibleAttribute(model, attributeName);
202
243
  if (isVisible2) {
203
- acc[attributeName] = { fields: ["documentId", "locale", "publishedAt"] };
244
+ acc[attributeName] = { [fieldSelector]: ["documentId", "locale", "publishedAt"] };
204
245
  }
205
246
  break;
206
247
  }
207
248
  case "media": {
208
- acc[attributeName] = { fields: ["id"] };
249
+ acc[attributeName] = { [fieldSelector]: ["id"] };
209
250
  break;
210
251
  }
211
252
  case "component": {
@@ -228,80 +269,69 @@ const createHistoryService = ({ strapi: strapi2 }) => {
228
269
  return acc;
229
270
  }, {});
230
271
  };
231
- return {
232
- async bootstrap() {
233
- if (state.isInitialized) {
234
- return;
235
- }
236
- strapi2.documents.use(async (context, next) => {
237
- if (!strapi2.requestContext.get()?.request.url.startsWith("/content-manager")) {
238
- return next();
272
+ const buildMediaResponse = async (values) => {
273
+ return values.slice(0, 25).reduce(
274
+ async (currentRelationDataPromise, entry) => {
275
+ const currentRelationData = await currentRelationDataPromise;
276
+ if (!entry) {
277
+ return currentRelationData;
239
278
  }
240
- if (context.action !== "create" && context.action !== "update" && context.action !== "publish" && context.action !== "unpublish" && context.action !== "discardDraft") {
241
- return next();
279
+ const relatedEntry = await strapi2.db.query("plugin::upload.file").findOne({ where: { id: entry.id } });
280
+ if (relatedEntry) {
281
+ currentRelationData.results.push(relatedEntry);
282
+ } else {
283
+ currentRelationData.meta.missingCount += 1;
242
284
  }
243
- const contentTypeUid = context.contentType.uid;
244
- if (!contentTypeUid.startsWith("api::")) {
245
- return next();
285
+ return currentRelationData;
286
+ },
287
+ Promise.resolve({
288
+ results: [],
289
+ meta: { missingCount: 0 }
290
+ })
291
+ );
292
+ };
293
+ const buildRelationReponse = async (values, attributeSchema) => {
294
+ return values.slice(0, 25).reduce(
295
+ async (currentRelationDataPromise, entry) => {
296
+ const currentRelationData = await currentRelationDataPromise;
297
+ if (!entry) {
298
+ return currentRelationData;
246
299
  }
247
- const result = await next();
248
- const documentContext = context.action === "create" ? { documentId: result.documentId, locale: context.params?.locale } : { documentId: context.params.documentId, locale: context.params?.locale };
249
- const defaultLocale = await getDefaultLocale();
250
- const locale = documentContext.locale || defaultLocale;
251
- const document = await strapi2.documents(contentTypeUid).findOne({
252
- documentId: documentContext.documentId,
253
- locale,
254
- populate: getDeepPopulate2(contentTypeUid)
255
- });
256
- const status = await getVersionStatus(contentTypeUid, document);
257
- const attributesSchema = strapi2.getModel(contentTypeUid).attributes;
258
- const componentsSchemas = Object.keys(
259
- attributesSchema
260
- ).reduce((currentComponentSchemas, key) => {
261
- const fieldSchema = attributesSchema[key];
262
- if (fieldSchema.type === "component") {
263
- const componentSchema = strapi2.getModel(fieldSchema.component).attributes;
264
- return {
265
- ...currentComponentSchemas,
266
- [fieldSchema.component]: componentSchema
267
- };
268
- }
269
- return currentComponentSchemas;
270
- }, {});
271
- await strapi2.db.transaction(async ({ onCommit }) => {
272
- onCommit(() => {
273
- this.createVersion({
274
- contentType: contentTypeUid,
275
- data: fp.omit(FIELDS_TO_IGNORE, document),
276
- schema: fp.omit(FIELDS_TO_IGNORE, attributesSchema),
277
- componentsSchemas,
278
- relatedDocumentId: documentContext.documentId,
279
- locale,
280
- status
281
- });
300
+ const relatedEntry = await strapi2.documents(attributeSchema.target).findOne({ documentId: entry.documentId, locale: entry.locale || void 0 });
301
+ if (relatedEntry) {
302
+ currentRelationData.results.push({
303
+ ...relatedEntry,
304
+ status: await getVersionStatus(attributeSchema.target, relatedEntry)
282
305
  });
283
- });
284
- return result;
285
- });
286
- const retentionDays = getRetentionDays(strapi2);
287
- state.deleteExpiredJob = nodeSchedule.scheduleJob("0 0 * * *", () => {
288
- const retentionDaysInMilliseconds = retentionDays * 24 * 60 * 60 * 1e3;
289
- const expirationDate = new Date(Date.now() - retentionDaysInMilliseconds);
290
- query.deleteMany({
291
- where: {
292
- created_at: {
293
- $lt: expirationDate.toISOString()
294
- }
295
- }
296
- });
297
- });
298
- state.isInitialized = true;
299
- },
300
- async destroy() {
301
- if (state.deleteExpiredJob) {
302
- state.deleteExpiredJob.cancel();
303
- }
304
- },
306
+ } else {
307
+ currentRelationData.meta.missingCount += 1;
308
+ }
309
+ return currentRelationData;
310
+ },
311
+ Promise.resolve({
312
+ results: [],
313
+ meta: { missingCount: 0 }
314
+ })
315
+ );
316
+ };
317
+ return {
318
+ getSchemaAttributesDiff,
319
+ getRelationRestoreValue,
320
+ getMediaRestoreValue,
321
+ getDefaultLocale,
322
+ isLocalizedContentType,
323
+ getLocaleDictionary,
324
+ getRetentionDays,
325
+ getVersionStatus,
326
+ getDeepPopulate: getDeepPopulate2,
327
+ buildMediaResponse,
328
+ buildRelationReponse
329
+ };
330
+ };
331
+ const createHistoryService = ({ strapi: strapi2 }) => {
332
+ const query = strapi2.db.query(HISTORY_VERSION_UID);
333
+ const serviceUtils = createServiceUtils({ strapi: strapi2 });
334
+ return {
305
335
  async createVersion(historyVersionData) {
306
336
  await query.create({
307
337
  data: {
@@ -312,7 +342,13 @@ const createHistoryService = ({ strapi: strapi2 }) => {
312
342
  });
313
343
  },
314
344
  async findVersionsPage(params) {
315
- const locale = params.query.locale || await getDefaultLocale();
345
+ const model = strapi2.getModel(params.query.contentType);
346
+ const isLocalizedContentType = serviceUtils.isLocalizedContentType(model);
347
+ const defaultLocale = await serviceUtils.getDefaultLocale();
348
+ let locale = null;
349
+ if (isLocalizedContentType) {
350
+ locale = params.query.locale || defaultLocale;
351
+ }
316
352
  const [{ results, pagination }, localeDictionary] = await Promise.all([
317
353
  query.findPage({
318
354
  ...params.query,
@@ -326,78 +362,34 @@ const createHistoryService = ({ strapi: strapi2 }) => {
326
362
  populate: ["createdBy"],
327
363
  orderBy: [{ createdAt: "desc" }]
328
364
  }),
329
- getLocaleDictionary()
365
+ serviceUtils.getLocaleDictionary()
330
366
  ]);
331
- const buildRelationReponse = async (values, attributeSchema) => {
332
- return values.slice(0, 25).reduce(
333
- async (currentRelationDataPromise, entry) => {
334
- const currentRelationData = await currentRelationDataPromise;
335
- if (!entry) {
336
- return currentRelationData;
337
- }
338
- const relatedEntry = await strapi2.documents(attributeSchema.target).findOne({ documentId: entry.documentId, locale: entry.locale || void 0 });
339
- const permissionChecker2 = getService$1("permission-checker").create({
340
- userAbility: params.state.userAbility,
341
- model: attributeSchema.target
342
- });
343
- const sanitizedEntry = await permissionChecker2.sanitizeOutput(relatedEntry);
344
- if (sanitizedEntry) {
345
- currentRelationData.results.push({
346
- ...sanitizedEntry,
347
- status: await getVersionStatus(attributeSchema.target, sanitizedEntry)
348
- });
349
- } else {
350
- currentRelationData.meta.missingCount += 1;
351
- }
352
- return currentRelationData;
353
- },
354
- Promise.resolve({
355
- results: [],
356
- meta: { missingCount: 0 }
357
- })
358
- );
359
- };
360
- const buildMediaResponse = async (values) => {
361
- return values.slice(0, 25).reduce(
362
- async (currentRelationDataPromise, entry) => {
363
- const currentRelationData = await currentRelationDataPromise;
364
- if (!entry) {
365
- return currentRelationData;
366
- }
367
- const permissionChecker2 = getService$1("permission-checker").create({
368
- userAbility: params.state.userAbility,
369
- model: "plugin::upload.file"
370
- });
371
- const relatedEntry = await strapi2.db.query("plugin::upload.file").findOne({ where: { id: entry.id } });
372
- const sanitizedEntry = await permissionChecker2.sanitizeOutput(relatedEntry);
373
- if (sanitizedEntry) {
374
- currentRelationData.results.push(sanitizedEntry);
375
- } else {
376
- currentRelationData.meta.missingCount += 1;
377
- }
378
- return currentRelationData;
379
- },
380
- Promise.resolve({
381
- results: [],
382
- meta: { missingCount: 0 }
383
- })
384
- );
385
- };
386
367
  const populateEntryRelations = async (entry) => {
387
368
  const entryWithRelations = await Object.entries(entry.schema).reduce(
388
369
  async (currentDataWithRelations, [attributeKey, attributeSchema]) => {
389
370
  const attributeValue = entry.data[attributeKey];
390
371
  const attributeValues = Array.isArray(attributeValue) ? attributeValue : [attributeValue];
391
372
  if (attributeSchema.type === "media") {
373
+ const permissionChecker2 = getService$1("permission-checker").create({
374
+ userAbility: params.state.userAbility,
375
+ model: "plugin::upload.file"
376
+ });
377
+ const response = await serviceUtils.buildMediaResponse(attributeValues);
378
+ const sanitizedResults = await Promise.all(
379
+ response.results.map((media) => permissionChecker2.sanitizeOutput(media))
380
+ );
392
381
  return {
393
382
  ...await currentDataWithRelations,
394
- [attributeKey]: await buildMediaResponse(attributeValues)
383
+ [attributeKey]: {
384
+ results: sanitizedResults,
385
+ meta: response.meta
386
+ }
395
387
  };
396
388
  }
397
389
  if (attributeSchema.type === "relation" && attributeSchema.relation !== "morphToOne" && attributeSchema.relation !== "morphToMany") {
398
390
  if (attributeSchema.target === "admin::user") {
399
391
  const adminUsers = await Promise.all(
400
- attributeValues.map(async (userToPopulate) => {
392
+ attributeValues.map((userToPopulate) => {
401
393
  if (userToPopulate == null) {
402
394
  return null;
403
395
  }
@@ -414,9 +406,23 @@ const createHistoryService = ({ strapi: strapi2 }) => {
414
406
  [attributeKey]: adminUsers
415
407
  };
416
408
  }
409
+ const permissionChecker2 = getService$1("permission-checker").create({
410
+ userAbility: params.state.userAbility,
411
+ model: attributeSchema.target
412
+ });
413
+ const response = await serviceUtils.buildRelationReponse(
414
+ attributeValues,
415
+ attributeSchema
416
+ );
417
+ const sanitizedResults = await Promise.all(
418
+ response.results.map((media) => permissionChecker2.sanitizeOutput(media))
419
+ );
417
420
  return {
418
421
  ...await currentDataWithRelations,
419
- [attributeKey]: await buildRelationReponse(attributeValues, attributeSchema)
422
+ [attributeKey]: {
423
+ results: sanitizedResults,
424
+ meta: response.meta
425
+ }
420
426
  };
421
427
  }
422
428
  return currentDataWithRelations;
@@ -431,7 +437,7 @@ const createHistoryService = ({ strapi: strapi2 }) => {
431
437
  ...result,
432
438
  data: await populateEntryRelations(result),
433
439
  meta: {
434
- unknownAttributes: getSchemaAttributesDiff(
440
+ unknownAttributes: serviceUtils.getSchemaAttributesDiff(
435
441
  result.schema,
436
442
  strapi2.getModel(params.query.contentType).attributes
437
443
  )
@@ -448,7 +454,10 @@ const createHistoryService = ({ strapi: strapi2 }) => {
448
454
  async restoreVersion(versionId) {
449
455
  const version = await query.findOne({ where: { id: versionId } });
450
456
  const contentTypeSchemaAttributes = strapi2.getModel(version.contentType).attributes;
451
- const schemaDiff = getSchemaAttributesDiff(version.schema, contentTypeSchemaAttributes);
457
+ const schemaDiff = serviceUtils.getSchemaAttributesDiff(
458
+ version.schema,
459
+ contentTypeSchemaAttributes
460
+ );
452
461
  const dataWithoutAddedAttributes = Object.keys(schemaDiff.added).reduce(
453
462
  (currentData, addedKey) => {
454
463
  currentData[addedKey] = null;
@@ -461,61 +470,26 @@ const createHistoryService = ({ strapi: strapi2 }) => {
461
470
  FIELDS_TO_IGNORE,
462
471
  contentTypeSchemaAttributes
463
472
  );
464
- const dataWithoutMissingRelations = await Object.entries(sanitizedSchemaAttributes).reduce(
465
- async (previousRelationAttributesPromise, [name, attribute]) => {
466
- const previousRelationAttributes = await previousRelationAttributesPromise;
467
- const relationData = version.data[name];
468
- if (relationData === null) {
473
+ const reducer = strapiUtils.async.reduce(Object.entries(sanitizedSchemaAttributes));
474
+ const dataWithoutMissingRelations = await reducer(
475
+ async (previousRelationAttributes, [name, attribute]) => {
476
+ const versionRelationData = version.data[name];
477
+ if (!versionRelationData) {
469
478
  return previousRelationAttributes;
470
479
  }
471
480
  if (attribute.type === "relation" && // TODO: handle polymorphic relations
472
481
  attribute.relation !== "morphToOne" && attribute.relation !== "morphToMany") {
473
- if (Array.isArray(relationData)) {
474
- if (relationData.length === 0)
475
- return previousRelationAttributes;
476
- const existingAndMissingRelations = await Promise.all(
477
- relationData.map((relation) => {
478
- return strapi2.documents(attribute.target).findOne({
479
- documentId: relation.documentId,
480
- locale: relation.locale || void 0
481
- });
482
- })
483
- );
484
- const existingRelations = existingAndMissingRelations.filter(
485
- (relation) => relation !== null
486
- );
487
- previousRelationAttributes[name] = existingRelations;
488
- } else {
489
- const existingRelation = await strapi2.documents(attribute.target).findOne({
490
- documentId: relationData.documentId,
491
- locale: relationData.locale || void 0
492
- });
493
- if (!existingRelation) {
494
- previousRelationAttributes[name] = null;
495
- }
496
- }
482
+ const data2 = await serviceUtils.getRelationRestoreValue(versionRelationData, attribute);
483
+ previousRelationAttributes[name] = data2;
497
484
  }
498
485
  if (attribute.type === "media") {
499
- if (attribute.multiple) {
500
- const existingAndMissingMedias = await Promise.all(
501
- // @ts-expect-error Fix the type definitions so this isn't any
502
- relationData.map((media) => {
503
- return strapi2.db.query("plugin::upload.file").findOne({ where: { id: media.id } });
504
- })
505
- );
506
- const existingMedias = existingAndMissingMedias.filter((media) => media != null);
507
- previousRelationAttributes[name] = existingMedias;
508
- } else {
509
- const existingMedia = await strapi2.db.query("plugin::upload.file").findOne({ where: { id: version.data[name].id } });
510
- if (!existingMedia) {
511
- previousRelationAttributes[name] = null;
512
- }
513
- }
486
+ const data2 = await serviceUtils.getMediaRestoreValue(versionRelationData, attribute);
487
+ previousRelationAttributes[name] = data2;
514
488
  }
515
489
  return previousRelationAttributes;
516
490
  },
517
491
  // Clone to avoid mutating the original version data
518
- Promise.resolve(structuredClone(dataWithoutAddedAttributes))
492
+ structuredClone(dataWithoutAddedAttributes)
519
493
  );
520
494
  const data = fp.omit(["id", ...Object.keys(schemaDiff.removed)], dataWithoutMissingRelations);
521
495
  const restoredDocument = await strapi2.documents(version.contentType).update({
@@ -530,8 +504,120 @@ const createHistoryService = ({ strapi: strapi2 }) => {
530
504
  }
531
505
  };
532
506
  };
507
+ const shouldCreateHistoryVersion = (context) => {
508
+ if (!strapi.requestContext.get()?.request.url.startsWith("/content-manager")) {
509
+ return false;
510
+ }
511
+ if (context.action !== "create" && context.action !== "update" && context.action !== "clone" && context.action !== "publish" && context.action !== "unpublish" && context.action !== "discardDraft") {
512
+ return false;
513
+ }
514
+ if (context.action === "update" && strapi.requestContext.get()?.request.url.endsWith("/actions/publish")) {
515
+ return false;
516
+ }
517
+ if (!context.contentType.uid.startsWith("api::")) {
518
+ return false;
519
+ }
520
+ return true;
521
+ };
522
+ const getSchemas = (uid2) => {
523
+ const attributesSchema = strapi.getModel(uid2).attributes;
524
+ const componentsSchemas = Object.keys(attributesSchema).reduce(
525
+ (currentComponentSchemas, key) => {
526
+ const fieldSchema = attributesSchema[key];
527
+ if (fieldSchema.type === "component") {
528
+ const componentSchema = strapi.getModel(fieldSchema.component).attributes;
529
+ return {
530
+ ...currentComponentSchemas,
531
+ [fieldSchema.component]: componentSchema
532
+ };
533
+ }
534
+ return currentComponentSchemas;
535
+ },
536
+ {}
537
+ );
538
+ return {
539
+ schema: fp.omit(FIELDS_TO_IGNORE, attributesSchema),
540
+ componentsSchemas
541
+ };
542
+ };
543
+ const createLifecyclesService = ({ strapi: strapi2 }) => {
544
+ const state = {
545
+ deleteExpiredJob: null,
546
+ isInitialized: false
547
+ };
548
+ const serviceUtils = createServiceUtils({ strapi: strapi2 });
549
+ return {
550
+ async bootstrap() {
551
+ if (state.isInitialized) {
552
+ return;
553
+ }
554
+ strapi2.documents.use(async (context, next) => {
555
+ const result = await next();
556
+ if (!shouldCreateHistoryVersion(context)) {
557
+ return result;
558
+ }
559
+ const documentId = context.action === "create" || context.action === "clone" ? result.documentId : context.params.documentId;
560
+ const defaultLocale = await serviceUtils.getDefaultLocale();
561
+ const locales = fp.castArray(context.params?.locale || defaultLocale);
562
+ if (!locales.length) {
563
+ return result;
564
+ }
565
+ const uid2 = context.contentType.uid;
566
+ const schemas = getSchemas(uid2);
567
+ const model = strapi2.getModel(uid2);
568
+ const isLocalizedContentType = serviceUtils.isLocalizedContentType(model);
569
+ const localeEntries = await strapi2.db.query(uid2).findMany({
570
+ where: {
571
+ documentId,
572
+ ...isLocalizedContentType ? { locale: { $in: locales } } : {},
573
+ ...strapiUtils.contentTypes.hasDraftAndPublish(strapi2.contentTypes[uid2]) ? { publishedAt: null } : {}
574
+ },
575
+ populate: serviceUtils.getDeepPopulate(
576
+ uid2,
577
+ true
578
+ /* use database syntax */
579
+ )
580
+ });
581
+ await strapi2.db.transaction(async ({ onCommit }) => {
582
+ onCommit(async () => {
583
+ for (const entry of localeEntries) {
584
+ const status = await serviceUtils.getVersionStatus(uid2, entry);
585
+ await getService(strapi2, "history").createVersion({
586
+ contentType: uid2,
587
+ data: fp.omit(FIELDS_TO_IGNORE, entry),
588
+ relatedDocumentId: documentId,
589
+ locale: entry.locale,
590
+ status,
591
+ ...schemas
592
+ });
593
+ }
594
+ });
595
+ });
596
+ return result;
597
+ });
598
+ state.deleteExpiredJob = nodeSchedule.scheduleJob("0 0 * * *", () => {
599
+ const retentionDaysInMilliseconds = serviceUtils.getRetentionDays() * 24 * 60 * 60 * 1e3;
600
+ const expirationDate = new Date(Date.now() - retentionDaysInMilliseconds);
601
+ strapi2.db.query(HISTORY_VERSION_UID).deleteMany({
602
+ where: {
603
+ created_at: {
604
+ $lt: expirationDate.toISOString()
605
+ }
606
+ }
607
+ });
608
+ });
609
+ state.isInitialized = true;
610
+ },
611
+ async destroy() {
612
+ if (state.deleteExpiredJob) {
613
+ state.deleteExpiredJob.cancel();
614
+ }
615
+ }
616
+ };
617
+ };
533
618
  const services$1 = {
534
- history: createHistoryService
619
+ history: createHistoryService,
620
+ lifecycles: createLifecyclesService
535
621
  };
536
622
  const info = { pluginName: "content-manager", type: "admin" };
537
623
  const historyVersionRouter = {
@@ -611,10 +697,10 @@ const getFeature = () => {
611
697
  strapi2.get("models").add(historyVersion);
612
698
  },
613
699
  bootstrap({ strapi: strapi2 }) {
614
- getService(strapi2, "history").bootstrap();
700
+ getService(strapi2, "lifecycles").bootstrap();
615
701
  },
616
702
  destroy({ strapi: strapi2 }) {
617
- getService(strapi2, "history").destroy();
703
+ getService(strapi2, "lifecycles").destroy();
618
704
  },
619
705
  controllers: controllers$1,
620
706
  services: services$1,
@@ -1144,6 +1230,11 @@ const { createPolicy } = strapiUtils.policy;
1144
1230
  const hasPermissions = createPolicy({
1145
1231
  name: "plugin::content-manager.hasPermissions",
1146
1232
  validator: validateHasPermissionsInput,
1233
+ /**
1234
+ * NOTE: Action aliases are currently not checked at this level (policy).
1235
+ * This is currently the intended behavior to avoid changing the behavior of API related permissions.
1236
+ * If you want to add support for it, please create a dedicated RFC with a list of potential side effect this could have.
1237
+ */
1147
1238
  handler(ctx, config = {}) {
1148
1239
  const { actions = [], hasAtLeastOne = false } = config;
1149
1240
  const { userAbility } = ctx.state;
@@ -1433,7 +1524,7 @@ const { PaginationError, ValidationError } = strapiUtils.errors;
1433
1524
  const TYPES = ["singleType", "collectionType"];
1434
1525
  const kindSchema = strapiUtils.yup.string().oneOf(TYPES).nullable();
1435
1526
  const bulkActionInputSchema = strapiUtils.yup.object({
1436
- ids: strapiUtils.yup.array().of(strapiUtils.yup.strapiID()).min(1).required()
1527
+ documentIds: strapiUtils.yup.array().of(strapiUtils.yup.strapiID()).min(1).required()
1437
1528
  }).required();
1438
1529
  const generateUIDInputSchema = strapiUtils.yup.object({
1439
1530
  contentTypeUID: strapiUtils.yup.string().required(),
@@ -1532,15 +1623,49 @@ const excludeNotCreatableFields = (uid2, permissionChecker2) => (body, path = []
1532
1623
  }
1533
1624
  }, body);
1534
1625
  };
1535
- const getDocumentLocaleAndStatus = (request) => {
1536
- const { locale, status, ...rest } = request || {};
1537
- if (!fp.isNil(locale) && typeof locale !== "string") {
1538
- throw new strapiUtils.errors.ValidationError(`Invalid locale: ${locale}`);
1539
- }
1540
- if (!fp.isNil(status) && !["draft", "published"].includes(status)) {
1541
- throw new strapiUtils.errors.ValidationError(`Invalid status: ${status}`);
1626
+ const singleLocaleSchema = strapiUtils.yup.string().nullable();
1627
+ const multipleLocaleSchema = strapiUtils.yup.lazy(
1628
+ (value) => Array.isArray(value) ? strapiUtils.yup.array().of(singleLocaleSchema.required()) : singleLocaleSchema
1629
+ );
1630
+ const statusSchema = strapiUtils.yup.mixed().oneOf(["draft", "published"], "Invalid status");
1631
+ const getDocumentLocaleAndStatus = async (request, model, opts = { allowMultipleLocales: false }) => {
1632
+ const { allowMultipleLocales } = opts;
1633
+ const { locale, status: providedStatus, ...rest } = request || {};
1634
+ const defaultStatus = strapiUtils.contentTypes.hasDraftAndPublish(strapi.getModel(model)) ? void 0 : "published";
1635
+ const status = providedStatus !== void 0 ? providedStatus : defaultStatus;
1636
+ const schema = strapiUtils.yup.object().shape({
1637
+ locale: allowMultipleLocales ? multipleLocaleSchema : singleLocaleSchema,
1638
+ status: statusSchema
1639
+ });
1640
+ try {
1641
+ await strapiUtils.validateYupSchema(schema, { strict: true, abortEarly: false })(request);
1642
+ return { locale, status, ...rest };
1643
+ } catch (error) {
1644
+ throw new strapiUtils.errors.ValidationError(`Validation error: ${error.message}`);
1542
1645
  }
1543
- return { locale, status, ...rest };
1646
+ };
1647
+ const formatDocumentWithMetadata = async (permissionChecker2, uid2, document, opts = {}) => {
1648
+ const documentMetadata2 = getService$1("document-metadata");
1649
+ const serviceOutput = await documentMetadata2.formatDocumentWithMetadata(uid2, document, opts);
1650
+ let {
1651
+ meta: { availableLocales, availableStatus }
1652
+ } = serviceOutput;
1653
+ const metadataSanitizer = permissionChecker2.sanitizeOutput;
1654
+ availableLocales = await strapiUtils.async.map(
1655
+ availableLocales,
1656
+ async (localeDocument) => metadataSanitizer(localeDocument)
1657
+ );
1658
+ availableStatus = await strapiUtils.async.map(
1659
+ availableStatus,
1660
+ async (statusDocument) => metadataSanitizer(statusDocument)
1661
+ );
1662
+ return {
1663
+ ...serviceOutput,
1664
+ meta: {
1665
+ availableLocales,
1666
+ availableStatus
1667
+ }
1668
+ };
1544
1669
  };
1545
1670
  const createDocument = async (ctx, opts) => {
1546
1671
  const { userAbility, user } = ctx.state;
@@ -1555,7 +1680,7 @@ const createDocument = async (ctx, opts) => {
1555
1680
  const setCreator = strapiUtils.setCreatorFields({ user });
1556
1681
  const sanitizeFn = strapiUtils.async.pipe(pickPermittedFields, setCreator);
1557
1682
  const sanitizedBody = await sanitizeFn(body);
1558
- const { locale, status = "draft" } = getDocumentLocaleAndStatus(body);
1683
+ const { locale, status } = await getDocumentLocaleAndStatus(body, model);
1559
1684
  return documentManager2.create(model, {
1560
1685
  data: sanitizedBody,
1561
1686
  locale,
@@ -1574,7 +1699,7 @@ const updateDocument = async (ctx, opts) => {
1574
1699
  }
1575
1700
  const permissionQuery = await permissionChecker2.sanitizedQuery.update(ctx.query);
1576
1701
  const populate = await getService$1("populate-builder")(model).populateFromQuery(permissionQuery).build();
1577
- const { locale } = getDocumentLocaleAndStatus(body);
1702
+ const { locale } = await getDocumentLocaleAndStatus(body, model);
1578
1703
  const [documentVersion, documentExists] = await Promise.all([
1579
1704
  documentManager2.findOne(id, model, { populate, locale, status: "draft" }),
1580
1705
  documentManager2.exists(model, id)
@@ -1612,7 +1737,7 @@ const collectionTypes = {
1612
1737
  }
1613
1738
  const permissionQuery = await permissionChecker2.sanitizedQuery.read(query);
1614
1739
  const populate = await getService$1("populate-builder")(model).populateFromQuery(permissionQuery).populateDeep(1).countRelations({ toOne: false, toMany: true }).build();
1615
- const { locale, status } = getDocumentLocaleAndStatus(query);
1740
+ const { locale, status } = await getDocumentLocaleAndStatus(query, model);
1616
1741
  const { results: documents, pagination } = await documentManager2.findPage(
1617
1742
  { ...permissionQuery, populate, locale, status },
1618
1743
  model
@@ -1641,14 +1766,13 @@ const collectionTypes = {
1641
1766
  const { userAbility } = ctx.state;
1642
1767
  const { model, id } = ctx.params;
1643
1768
  const documentManager2 = getService$1("document-manager");
1644
- const documentMetadata2 = getService$1("document-metadata");
1645
1769
  const permissionChecker2 = getService$1("permission-checker").create({ userAbility, model });
1646
1770
  if (permissionChecker2.cannot.read()) {
1647
1771
  return ctx.forbidden();
1648
1772
  }
1649
1773
  const permissionQuery = await permissionChecker2.sanitizedQuery.read(ctx.query);
1650
1774
  const populate = await getService$1("populate-builder")(model).populateFromQuery(permissionQuery).populateDeep(Infinity).countRelations().build();
1651
- const { locale, status = "draft" } = getDocumentLocaleAndStatus(ctx.query);
1775
+ const { locale, status } = await getDocumentLocaleAndStatus(ctx.query, model);
1652
1776
  const version = await documentManager2.findOne(id, model, {
1653
1777
  populate,
1654
1778
  locale,
@@ -1659,9 +1783,11 @@ const collectionTypes = {
1659
1783
  if (!exists) {
1660
1784
  return ctx.notFound();
1661
1785
  }
1662
- const { meta } = await documentMetadata2.formatDocumentWithMetadata(
1786
+ const { meta } = await formatDocumentWithMetadata(
1787
+ permissionChecker2,
1663
1788
  model,
1664
- { id, locale, publishedAt: null },
1789
+ // @ts-expect-error TODO: fix
1790
+ { documentId: id, locale, publishedAt: null },
1665
1791
  { availableLocales: true, availableStatus: false }
1666
1792
  );
1667
1793
  ctx.body = { data: {}, meta };
@@ -1671,12 +1797,11 @@ const collectionTypes = {
1671
1797
  return ctx.forbidden();
1672
1798
  }
1673
1799
  const sanitizedDocument = await permissionChecker2.sanitizeOutput(version);
1674
- ctx.body = await documentMetadata2.formatDocumentWithMetadata(model, sanitizedDocument);
1800
+ ctx.body = await formatDocumentWithMetadata(permissionChecker2, model, sanitizedDocument);
1675
1801
  },
1676
1802
  async create(ctx) {
1677
1803
  const { userAbility } = ctx.state;
1678
1804
  const { model } = ctx.params;
1679
- const documentMetadata2 = getService$1("document-metadata");
1680
1805
  const permissionChecker2 = getService$1("permission-checker").create({ userAbility, model });
1681
1806
  const [totalEntries, document] = await Promise.all([
1682
1807
  strapi.db.query(model).count(),
@@ -1684,7 +1809,7 @@ const collectionTypes = {
1684
1809
  ]);
1685
1810
  const sanitizedDocument = await permissionChecker2.sanitizeOutput(document);
1686
1811
  ctx.status = 201;
1687
- ctx.body = await documentMetadata2.formatDocumentWithMetadata(model, sanitizedDocument, {
1812
+ ctx.body = await formatDocumentWithMetadata(permissionChecker2, model, sanitizedDocument, {
1688
1813
  // Empty metadata as it's not relevant for a new document
1689
1814
  availableLocales: false,
1690
1815
  availableStatus: false
@@ -1698,25 +1823,23 @@ const collectionTypes = {
1698
1823
  async update(ctx) {
1699
1824
  const { userAbility } = ctx.state;
1700
1825
  const { model } = ctx.params;
1701
- const documentMetadata2 = getService$1("document-metadata");
1702
1826
  const permissionChecker2 = getService$1("permission-checker").create({ userAbility, model });
1703
1827
  const updatedVersion = await updateDocument(ctx);
1704
1828
  const sanitizedVersion = await permissionChecker2.sanitizeOutput(updatedVersion);
1705
- ctx.body = await documentMetadata2.formatDocumentWithMetadata(model, sanitizedVersion);
1829
+ ctx.body = await formatDocumentWithMetadata(permissionChecker2, model, sanitizedVersion);
1706
1830
  },
1707
1831
  async clone(ctx) {
1708
1832
  const { userAbility, user } = ctx.state;
1709
1833
  const { model, sourceId: id } = ctx.params;
1710
1834
  const { body } = ctx.request;
1711
1835
  const documentManager2 = getService$1("document-manager");
1712
- const documentMetadata2 = getService$1("document-metadata");
1713
1836
  const permissionChecker2 = getService$1("permission-checker").create({ userAbility, model });
1714
1837
  if (permissionChecker2.cannot.create()) {
1715
1838
  return ctx.forbidden();
1716
1839
  }
1717
1840
  const permissionQuery = await permissionChecker2.sanitizedQuery.create(ctx.query);
1718
1841
  const populate = await getService$1("populate-builder")(model).populateFromQuery(permissionQuery).build();
1719
- const { locale } = getDocumentLocaleAndStatus(body);
1842
+ const { locale } = await getDocumentLocaleAndStatus(body, model);
1720
1843
  const document = await documentManager2.findOne(id, model, {
1721
1844
  populate,
1722
1845
  locale,
@@ -1732,7 +1855,7 @@ const collectionTypes = {
1732
1855
  const sanitizedBody = await sanitizeFn(body);
1733
1856
  const clonedDocument = await documentManager2.clone(document.documentId, sanitizedBody, model);
1734
1857
  const sanitizedDocument = await permissionChecker2.sanitizeOutput(clonedDocument);
1735
- ctx.body = await documentMetadata2.formatDocumentWithMetadata(model, sanitizedDocument, {
1858
+ ctx.body = await formatDocumentWithMetadata(permissionChecker2, model, sanitizedDocument, {
1736
1859
  // Empty metadata as it's not relevant for a new document
1737
1860
  availableLocales: false,
1738
1861
  availableStatus: false
@@ -1761,7 +1884,7 @@ const collectionTypes = {
1761
1884
  }
1762
1885
  const permissionQuery = await permissionChecker2.sanitizedQuery.delete(ctx.query);
1763
1886
  const populate = await getService$1("populate-builder")(model).populateFromQuery(permissionQuery).build();
1764
- const { locale } = getDocumentLocaleAndStatus(ctx.query);
1887
+ const { locale } = await getDocumentLocaleAndStatus(ctx.query, model);
1765
1888
  const documentLocales = await documentManager2.findLocales(id, model, { populate, locale });
1766
1889
  if (documentLocales.length === 0) {
1767
1890
  return ctx.notFound();
@@ -1783,7 +1906,6 @@ const collectionTypes = {
1783
1906
  const { id, model } = ctx.params;
1784
1907
  const { body } = ctx.request;
1785
1908
  const documentManager2 = getService$1("document-manager");
1786
- const documentMetadata2 = getService$1("document-metadata");
1787
1909
  const permissionChecker2 = getService$1("permission-checker").create({ userAbility, model });
1788
1910
  if (permissionChecker2.cannot.publish()) {
1789
1911
  return ctx.forbidden();
@@ -1791,25 +1913,46 @@ const collectionTypes = {
1791
1913
  const publishedDocument = await strapi.db.transaction(async () => {
1792
1914
  const permissionQuery = await permissionChecker2.sanitizedQuery.publish(ctx.query);
1793
1915
  const populate = await getService$1("populate-builder")(model).populateFromQuery(permissionQuery).populateDeep(Infinity).countRelations().build();
1794
- const document = id ? await updateDocument(ctx, { populate }) : await createDocument(ctx, { populate });
1916
+ let document;
1917
+ const { locale } = await getDocumentLocaleAndStatus(body, model);
1918
+ const isCreate = fp.isNil(id);
1919
+ if (isCreate) {
1920
+ if (permissionChecker2.cannot.create()) {
1921
+ throw new strapiUtils.errors.ForbiddenError();
1922
+ }
1923
+ document = await createDocument(ctx, { populate });
1924
+ }
1925
+ const isUpdate = !isCreate;
1926
+ if (isUpdate) {
1927
+ document = await documentManager2.findOne(id, model, { populate, locale });
1928
+ if (!document) {
1929
+ throw new strapiUtils.errors.NotFoundError("Document not found");
1930
+ }
1931
+ if (permissionChecker2.can.update(document)) {
1932
+ await updateDocument(ctx);
1933
+ }
1934
+ }
1795
1935
  if (permissionChecker2.cannot.publish(document)) {
1796
1936
  throw new strapiUtils.errors.ForbiddenError();
1797
1937
  }
1798
- const { locale } = getDocumentLocaleAndStatus(body);
1799
- return documentManager2.publish(document.documentId, model, {
1938
+ const publishResult = await documentManager2.publish(document.documentId, model, {
1800
1939
  locale
1801
1940
  // TODO: Allow setting creator fields on publish
1802
1941
  // data: setCreatorFields({ user, isEdition: true })({}),
1803
1942
  });
1943
+ if (!publishResult || publishResult.length === 0) {
1944
+ throw new strapiUtils.errors.NotFoundError("Document not found or already published.");
1945
+ }
1946
+ return publishResult[0];
1804
1947
  });
1805
1948
  const sanitizedDocument = await permissionChecker2.sanitizeOutput(publishedDocument);
1806
- ctx.body = await documentMetadata2.formatDocumentWithMetadata(model, sanitizedDocument);
1949
+ ctx.body = await formatDocumentWithMetadata(permissionChecker2, model, sanitizedDocument);
1807
1950
  },
1808
1951
  async bulkPublish(ctx) {
1809
1952
  const { userAbility } = ctx.state;
1810
1953
  const { model } = ctx.params;
1811
1954
  const { body } = ctx.request;
1812
- const { ids } = body;
1955
+ const { documentIds } = body;
1813
1956
  await validateBulkActionInput(body);
1814
1957
  const documentManager2 = getService$1("document-manager");
1815
1958
  const permissionChecker2 = getService$1("permission-checker").create({ userAbility, model });
@@ -1818,8 +1961,13 @@ const collectionTypes = {
1818
1961
  }
1819
1962
  const permissionQuery = await permissionChecker2.sanitizedQuery.publish(ctx.query);
1820
1963
  const populate = await getService$1("populate-builder")(model).populateFromQuery(permissionQuery).populateDeep(Infinity).countRelations().build();
1821
- const entityPromises = ids.map((id) => documentManager2.findOne(id, model, { populate }));
1822
- const entities = await Promise.all(entityPromises);
1964
+ const { locale } = await getDocumentLocaleAndStatus(body, model, {
1965
+ allowMultipleLocales: true
1966
+ });
1967
+ const entityPromises = documentIds.map(
1968
+ (documentId) => documentManager2.findLocales(documentId, model, { populate, locale, isPublished: false })
1969
+ );
1970
+ const entities = (await Promise.all(entityPromises)).flat();
1823
1971
  for (const entity of entities) {
1824
1972
  if (!entity) {
1825
1973
  return ctx.notFound();
@@ -1828,24 +1976,27 @@ const collectionTypes = {
1828
1976
  return ctx.forbidden();
1829
1977
  }
1830
1978
  }
1831
- const { count } = await documentManager2.publishMany(entities, model);
1979
+ const count = await documentManager2.publishMany(model, documentIds, locale);
1832
1980
  ctx.body = { count };
1833
1981
  },
1834
1982
  async bulkUnpublish(ctx) {
1835
1983
  const { userAbility } = ctx.state;
1836
1984
  const { model } = ctx.params;
1837
1985
  const { body } = ctx.request;
1838
- const { ids } = body;
1986
+ const { documentIds } = body;
1839
1987
  await validateBulkActionInput(body);
1840
1988
  const documentManager2 = getService$1("document-manager");
1841
1989
  const permissionChecker2 = getService$1("permission-checker").create({ userAbility, model });
1842
1990
  if (permissionChecker2.cannot.unpublish()) {
1843
1991
  return ctx.forbidden();
1844
1992
  }
1845
- const permissionQuery = await permissionChecker2.sanitizedQuery.publish(ctx.query);
1846
- const populate = await getService$1("populate-builder")(model).populateFromQuery(permissionQuery).build();
1847
- const entityPromises = ids.map((id) => documentManager2.findOne(id, model, { populate }));
1848
- const entities = await Promise.all(entityPromises);
1993
+ const { locale } = await getDocumentLocaleAndStatus(body, model, {
1994
+ allowMultipleLocales: true
1995
+ });
1996
+ const entityPromises = documentIds.map(
1997
+ (documentId) => documentManager2.findLocales(documentId, model, { locale, isPublished: true })
1998
+ );
1999
+ const entities = (await Promise.all(entityPromises)).flat();
1849
2000
  for (const entity of entities) {
1850
2001
  if (!entity) {
1851
2002
  return ctx.notFound();
@@ -1854,7 +2005,8 @@ const collectionTypes = {
1854
2005
  return ctx.forbidden();
1855
2006
  }
1856
2007
  }
1857
- const { count } = await documentManager2.unpublishMany(entities, model);
2008
+ const entitiesIds = entities.map((document) => document.documentId);
2009
+ const { count } = await documentManager2.unpublishMany(entitiesIds, model, { locale });
1858
2010
  ctx.body = { count };
1859
2011
  },
1860
2012
  async unpublish(ctx) {
@@ -1864,7 +2016,6 @@ const collectionTypes = {
1864
2016
  body: { discardDraft, ...body }
1865
2017
  } = ctx.request;
1866
2018
  const documentManager2 = getService$1("document-manager");
1867
- const documentMetadata2 = getService$1("document-metadata");
1868
2019
  const permissionChecker2 = getService$1("permission-checker").create({ userAbility, model });
1869
2020
  if (permissionChecker2.cannot.unpublish()) {
1870
2021
  return ctx.forbidden();
@@ -1874,7 +2025,7 @@ const collectionTypes = {
1874
2025
  }
1875
2026
  const permissionQuery = await permissionChecker2.sanitizedQuery.unpublish(ctx.query);
1876
2027
  const populate = await getService$1("populate-builder")(model).populateFromQuery(permissionQuery).build();
1877
- const { locale } = getDocumentLocaleAndStatus(body);
2028
+ const { locale } = await getDocumentLocaleAndStatus(body, model);
1878
2029
  const document = await documentManager2.findOne(id, model, {
1879
2030
  populate,
1880
2031
  locale,
@@ -1896,7 +2047,7 @@ const collectionTypes = {
1896
2047
  ctx.body = await strapiUtils.async.pipe(
1897
2048
  (document2) => documentManager2.unpublish(document2.documentId, model, { locale }),
1898
2049
  permissionChecker2.sanitizeOutput,
1899
- (document2) => documentMetadata2.formatDocumentWithMetadata(model, document2)
2050
+ (document2) => formatDocumentWithMetadata(permissionChecker2, model, document2)
1900
2051
  )(document);
1901
2052
  });
1902
2053
  },
@@ -1905,14 +2056,13 @@ const collectionTypes = {
1905
2056
  const { id, model } = ctx.params;
1906
2057
  const { body } = ctx.request;
1907
2058
  const documentManager2 = getService$1("document-manager");
1908
- const documentMetadata2 = getService$1("document-metadata");
1909
2059
  const permissionChecker2 = getService$1("permission-checker").create({ userAbility, model });
1910
2060
  if (permissionChecker2.cannot.discard()) {
1911
2061
  return ctx.forbidden();
1912
2062
  }
1913
2063
  const permissionQuery = await permissionChecker2.sanitizedQuery.discard(ctx.query);
1914
2064
  const populate = await getService$1("populate-builder")(model).populateFromQuery(permissionQuery).build();
1915
- const { locale } = getDocumentLocaleAndStatus(body);
2065
+ const { locale } = await getDocumentLocaleAndStatus(body, model);
1916
2066
  const document = await documentManager2.findOne(id, model, {
1917
2067
  populate,
1918
2068
  locale,
@@ -1927,14 +2077,14 @@ const collectionTypes = {
1927
2077
  ctx.body = await strapiUtils.async.pipe(
1928
2078
  (document2) => documentManager2.discardDraft(document2.documentId, model, { locale }),
1929
2079
  permissionChecker2.sanitizeOutput,
1930
- (document2) => documentMetadata2.formatDocumentWithMetadata(model, document2)
2080
+ (document2) => formatDocumentWithMetadata(permissionChecker2, model, document2)
1931
2081
  )(document);
1932
2082
  },
1933
2083
  async bulkDelete(ctx) {
1934
2084
  const { userAbility } = ctx.state;
1935
2085
  const { model } = ctx.params;
1936
2086
  const { query, body } = ctx.request;
1937
- const { ids } = body;
2087
+ const { documentIds } = body;
1938
2088
  await validateBulkActionInput(body);
1939
2089
  const documentManager2 = getService$1("document-manager");
1940
2090
  const permissionChecker2 = getService$1("permission-checker").create({ userAbility, model });
@@ -1942,14 +2092,22 @@ const collectionTypes = {
1942
2092
  return ctx.forbidden();
1943
2093
  }
1944
2094
  const permissionQuery = await permissionChecker2.sanitizedQuery.delete(query);
1945
- const idsWhereClause = { id: { $in: ids } };
1946
- const params = {
1947
- ...permissionQuery,
1948
- filters: {
1949
- $and: [idsWhereClause].concat(permissionQuery.filters || [])
2095
+ const populate = await getService$1("populate-builder")(model).populateFromQuery(permissionQuery).build();
2096
+ const { locale } = await getDocumentLocaleAndStatus(body, model);
2097
+ const documentLocales = await documentManager2.findLocales(documentIds, model, {
2098
+ populate,
2099
+ locale
2100
+ });
2101
+ if (documentLocales.length === 0) {
2102
+ return ctx.notFound();
2103
+ }
2104
+ for (const document of documentLocales) {
2105
+ if (permissionChecker2.cannot.delete(document)) {
2106
+ return ctx.forbidden();
1950
2107
  }
1951
- };
1952
- const { count } = await documentManager2.deleteMany(params, model);
2108
+ }
2109
+ const localeDocumentsIds = documentLocales.map((document) => document.documentId);
2110
+ const { count } = await documentManager2.deleteMany(localeDocumentsIds, model, { locale });
1953
2111
  ctx.body = { count };
1954
2112
  },
1955
2113
  async countDraftRelations(ctx) {
@@ -1962,7 +2120,7 @@ const collectionTypes = {
1962
2120
  }
1963
2121
  const permissionQuery = await permissionChecker2.sanitizedQuery.read(ctx.query);
1964
2122
  const populate = await getService$1("populate-builder")(model).populateFromQuery(permissionQuery).build();
1965
- const { locale, status = "draft" } = getDocumentLocaleAndStatus(ctx.query);
2123
+ const { locale, status } = await getDocumentLocaleAndStatus(ctx.query, model);
1966
2124
  const entity = await documentManager2.findOne(id, model, { populate, locale, status });
1967
2125
  if (!entity) {
1968
2126
  return ctx.notFound();
@@ -1977,7 +2135,7 @@ const collectionTypes = {
1977
2135
  },
1978
2136
  async countManyEntriesDraftRelations(ctx) {
1979
2137
  const { userAbility } = ctx.state;
1980
- const ids = ctx.request.query.ids;
2138
+ const ids = ctx.request.query.documentIds;
1981
2139
  const locale = ctx.request.query.locale;
1982
2140
  const { model } = ctx.params;
1983
2141
  const documentManager2 = getService$1("document-manager");
@@ -1985,16 +2143,16 @@ const collectionTypes = {
1985
2143
  if (permissionChecker2.cannot.read()) {
1986
2144
  return ctx.forbidden();
1987
2145
  }
1988
- const entities = await documentManager2.findMany(
2146
+ const documents = await documentManager2.findMany(
1989
2147
  {
1990
2148
  filters: {
1991
- id: ids
2149
+ documentId: ids
1992
2150
  },
1993
2151
  locale
1994
2152
  },
1995
2153
  model
1996
2154
  );
1997
- if (!entities) {
2155
+ if (!documents) {
1998
2156
  return ctx.notFound();
1999
2157
  }
2000
2158
  const number = await documentManager2.countManyEntriesDraftRelations(ids, model, locale);
@@ -2185,20 +2343,13 @@ const sanitizeMainField = (model, mainField, userAbility) => {
2185
2343
  userAbility,
2186
2344
  model: model.uid
2187
2345
  });
2188
- if (!isListable(model, mainField)) {
2346
+ const isMainFieldListable = isListable(model, mainField);
2347
+ const canReadMainField = permissionChecker2.can.read(null, mainField);
2348
+ if (!isMainFieldListable || !canReadMainField) {
2189
2349
  return "id";
2190
2350
  }
2191
- if (permissionChecker2.cannot.read(null, mainField)) {
2192
- if (model.uid === "plugin::users-permissions.role") {
2193
- const userPermissionChecker = getService$1("permission-checker").create({
2194
- userAbility,
2195
- model: "plugin::users-permissions.user"
2196
- });
2197
- if (userPermissionChecker.can.read()) {
2198
- return "name";
2199
- }
2200
- }
2201
- return "id";
2351
+ if (model.uid === "plugin::users-permissions.role") {
2352
+ return "name";
2202
2353
  }
2203
2354
  return mainField;
2204
2355
  };
@@ -2398,8 +2549,9 @@ const relations = {
2398
2549
  } else {
2399
2550
  where.id = id;
2400
2551
  }
2401
- if (status) {
2402
- where[`${alias}.published_at`] = getPublishedAtClause(status, targetUid);
2552
+ const publishedAt = getPublishedAtClause(status, targetUid);
2553
+ if (!fp.isEmpty(publishedAt)) {
2554
+ where[`${alias}.published_at`] = publishedAt;
2403
2555
  }
2404
2556
  if (filterByLocale) {
2405
2557
  where[`${alias}.locale`] = locale;
@@ -2456,9 +2608,7 @@ const relations = {
2456
2608
  addFiltersClause(permissionQuery, { id: { $in: loadedIds } });
2457
2609
  const sanitizedRes = await loadRelations({ id: entryId }, targetField, {
2458
2610
  ...strapi.get("query-params").transform(targetUid, permissionQuery),
2459
- ordering: "desc",
2460
- page: ctx.request.query.page,
2461
- pageSize: ctx.request.query.pageSize
2611
+ ordering: "desc"
2462
2612
  });
2463
2613
  const relationsUnion = fp.uniqBy("id", fp.concat(sanitizedRes.results, res.results));
2464
2614
  ctx.body = {
@@ -2490,7 +2640,7 @@ const createOrUpdateDocument = async (ctx, opts) => {
2490
2640
  throw new strapiUtils.errors.ForbiddenError();
2491
2641
  }
2492
2642
  const sanitizedQuery = await permissionChecker2.sanitizedQuery.update(query);
2493
- const { locale } = getDocumentLocaleAndStatus(body);
2643
+ const { locale } = await getDocumentLocaleAndStatus(body, model);
2494
2644
  const [documentVersion, otherDocumentVersion] = await Promise.all([
2495
2645
  findDocument(sanitizedQuery, model, { locale, status: "draft" }),
2496
2646
  // Find the first document to check if it exists
@@ -2527,12 +2677,11 @@ const singleTypes = {
2527
2677
  const { model } = ctx.params;
2528
2678
  const { query = {} } = ctx.request;
2529
2679
  const permissionChecker2 = getService$1("permission-checker").create({ userAbility, model });
2530
- const documentMetadata2 = getService$1("document-metadata");
2531
2680
  if (permissionChecker2.cannot.read()) {
2532
2681
  return ctx.forbidden();
2533
2682
  }
2534
2683
  const permissionQuery = await permissionChecker2.sanitizedQuery.read(query);
2535
- const { locale, status } = getDocumentLocaleAndStatus(query);
2684
+ const { locale, status } = await getDocumentLocaleAndStatus(query, model);
2536
2685
  const version = await findDocument(permissionQuery, model, { locale, status });
2537
2686
  if (!version) {
2538
2687
  if (permissionChecker2.cannot.create()) {
@@ -2542,8 +2691,10 @@ const singleTypes = {
2542
2691
  if (!document) {
2543
2692
  return ctx.notFound();
2544
2693
  }
2545
- const { meta } = await documentMetadata2.formatDocumentWithMetadata(
2694
+ const { meta } = await formatDocumentWithMetadata(
2695
+ permissionChecker2,
2546
2696
  model,
2697
+ // @ts-expect-error - fix types
2547
2698
  { id: document.documentId, locale, publishedAt: null },
2548
2699
  { availableLocales: true, availableStatus: false }
2549
2700
  );
@@ -2554,16 +2705,15 @@ const singleTypes = {
2554
2705
  return ctx.forbidden();
2555
2706
  }
2556
2707
  const sanitizedDocument = await permissionChecker2.sanitizeOutput(version);
2557
- ctx.body = await documentMetadata2.formatDocumentWithMetadata(model, sanitizedDocument);
2708
+ ctx.body = await formatDocumentWithMetadata(permissionChecker2, model, sanitizedDocument);
2558
2709
  },
2559
2710
  async createOrUpdate(ctx) {
2560
2711
  const { userAbility } = ctx.state;
2561
2712
  const { model } = ctx.params;
2562
- const documentMetadata2 = getService$1("document-metadata");
2563
2713
  const permissionChecker2 = getService$1("permission-checker").create({ userAbility, model });
2564
2714
  const document = await createOrUpdateDocument(ctx);
2565
2715
  const sanitizedDocument = await permissionChecker2.sanitizeOutput(document);
2566
- ctx.body = await documentMetadata2.formatDocumentWithMetadata(model, sanitizedDocument);
2716
+ ctx.body = await formatDocumentWithMetadata(permissionChecker2, model, sanitizedDocument);
2567
2717
  },
2568
2718
  async delete(ctx) {
2569
2719
  const { userAbility } = ctx.state;
@@ -2576,7 +2726,7 @@ const singleTypes = {
2576
2726
  }
2577
2727
  const sanitizedQuery = await permissionChecker2.sanitizedQuery.delete(query);
2578
2728
  const populate = await buildPopulateFromQuery(sanitizedQuery, model);
2579
- const { locale } = getDocumentLocaleAndStatus(query);
2729
+ const { locale } = await getDocumentLocaleAndStatus(query, model);
2580
2730
  const documentLocales = await documentManager2.findLocales(void 0, model, {
2581
2731
  populate,
2582
2732
  locale
@@ -2599,7 +2749,6 @@ const singleTypes = {
2599
2749
  const { model } = ctx.params;
2600
2750
  const { query = {} } = ctx.request;
2601
2751
  const documentManager2 = getService$1("document-manager");
2602
- const documentMetadata2 = getService$1("document-metadata");
2603
2752
  const permissionChecker2 = getService$1("permission-checker").create({ userAbility, model });
2604
2753
  if (permissionChecker2.cannot.publish()) {
2605
2754
  return ctx.forbidden();
@@ -2614,11 +2763,12 @@ const singleTypes = {
2614
2763
  if (permissionChecker2.cannot.publish(document)) {
2615
2764
  throw new strapiUtils.errors.ForbiddenError();
2616
2765
  }
2617
- const { locale } = getDocumentLocaleAndStatus(document);
2618
- return documentManager2.publish(document.documentId, model, { locale });
2766
+ const { locale } = await getDocumentLocaleAndStatus(document, model);
2767
+ const publishResult = await documentManager2.publish(document.documentId, model, { locale });
2768
+ return publishResult.at(0);
2619
2769
  });
2620
2770
  const sanitizedDocument = await permissionChecker2.sanitizeOutput(publishedDocument);
2621
- ctx.body = await documentMetadata2.formatDocumentWithMetadata(model, sanitizedDocument);
2771
+ ctx.body = await formatDocumentWithMetadata(permissionChecker2, model, sanitizedDocument);
2622
2772
  },
2623
2773
  async unpublish(ctx) {
2624
2774
  const { userAbility } = ctx.state;
@@ -2628,7 +2778,6 @@ const singleTypes = {
2628
2778
  query = {}
2629
2779
  } = ctx.request;
2630
2780
  const documentManager2 = getService$1("document-manager");
2631
- const documentMetadata2 = getService$1("document-metadata");
2632
2781
  const permissionChecker2 = getService$1("permission-checker").create({ userAbility, model });
2633
2782
  if (permissionChecker2.cannot.unpublish()) {
2634
2783
  return ctx.forbidden();
@@ -2637,7 +2786,7 @@ const singleTypes = {
2637
2786
  return ctx.forbidden();
2638
2787
  }
2639
2788
  const sanitizedQuery = await permissionChecker2.sanitizedQuery.unpublish(query);
2640
- const { locale } = getDocumentLocaleAndStatus(body);
2789
+ const { locale } = await getDocumentLocaleAndStatus(body, model);
2641
2790
  const document = await findDocument(sanitizedQuery, model, { locale });
2642
2791
  if (!document) {
2643
2792
  return ctx.notFound();
@@ -2655,7 +2804,7 @@ const singleTypes = {
2655
2804
  ctx.body = await strapiUtils.async.pipe(
2656
2805
  (document2) => documentManager2.unpublish(document2.documentId, model, { locale }),
2657
2806
  permissionChecker2.sanitizeOutput,
2658
- (document2) => documentMetadata2.formatDocumentWithMetadata(model, document2)
2807
+ (document2) => formatDocumentWithMetadata(permissionChecker2, model, document2)
2659
2808
  )(document);
2660
2809
  });
2661
2810
  },
@@ -2664,13 +2813,12 @@ const singleTypes = {
2664
2813
  const { model } = ctx.params;
2665
2814
  const { body, query = {} } = ctx.request;
2666
2815
  const documentManager2 = getService$1("document-manager");
2667
- const documentMetadata2 = getService$1("document-metadata");
2668
2816
  const permissionChecker2 = getService$1("permission-checker").create({ userAbility, model });
2669
2817
  if (permissionChecker2.cannot.discard()) {
2670
2818
  return ctx.forbidden();
2671
2819
  }
2672
2820
  const sanitizedQuery = await permissionChecker2.sanitizedQuery.discard(query);
2673
- const { locale } = getDocumentLocaleAndStatus(body);
2821
+ const { locale } = await getDocumentLocaleAndStatus(body, model);
2674
2822
  const document = await findDocument(sanitizedQuery, model, { locale, status: "published" });
2675
2823
  if (!document) {
2676
2824
  return ctx.notFound();
@@ -2681,7 +2829,7 @@ const singleTypes = {
2681
2829
  ctx.body = await strapiUtils.async.pipe(
2682
2830
  (document2) => documentManager2.discardDraft(document2.documentId, model, { locale }),
2683
2831
  permissionChecker2.sanitizeOutput,
2684
- (document2) => documentMetadata2.formatDocumentWithMetadata(model, document2)
2832
+ (document2) => formatDocumentWithMetadata(permissionChecker2, model, document2)
2685
2833
  )(document);
2686
2834
  },
2687
2835
  async countDraftRelations(ctx) {
@@ -2690,7 +2838,7 @@ const singleTypes = {
2690
2838
  const { query } = ctx.request;
2691
2839
  const documentManager2 = getService$1("document-manager");
2692
2840
  const permissionChecker2 = getService$1("permission-checker").create({ userAbility, model });
2693
- const { locale } = getDocumentLocaleAndStatus(query);
2841
+ const { locale } = await getDocumentLocaleAndStatus(query, model);
2694
2842
  if (permissionChecker2.cannot.read()) {
2695
2843
  return ctx.forbidden();
2696
2844
  }
@@ -2711,7 +2859,7 @@ const uid$1 = {
2711
2859
  async generateUID(ctx) {
2712
2860
  const { contentTypeUID, field, data } = await validateGenerateUIDInput(ctx.request.body);
2713
2861
  const { query = {} } = ctx.request;
2714
- const { locale } = getDocumentLocaleAndStatus(query);
2862
+ const { locale } = await getDocumentLocaleAndStatus(query, contentTypeUID);
2715
2863
  await validateUIDField(contentTypeUID, field);
2716
2864
  const uidService = getService$1("uid");
2717
2865
  ctx.body = {
@@ -2723,7 +2871,7 @@ const uid$1 = {
2723
2871
  ctx.request.body
2724
2872
  );
2725
2873
  const { query = {} } = ctx.request;
2726
- const { locale } = getDocumentLocaleAndStatus(query);
2874
+ const { locale } = await getDocumentLocaleAndStatus(query, contentTypeUID);
2727
2875
  await validateUIDField(contentTypeUID, field);
2728
2876
  const uidService = getService$1("uid");
2729
2877
  const isAvailable = await uidService.checkUIDAvailability({
@@ -3366,12 +3514,27 @@ const createPermissionChecker = (strapi2) => ({ userAbility, model }) => {
3366
3514
  ability: userAbility,
3367
3515
  model
3368
3516
  });
3369
- const toSubject = (entity) => entity ? permissionsManager.toSubject(entity, model) : model;
3517
+ const { actionProvider } = strapi2.service("admin::permission");
3518
+ const toSubject = (entity) => {
3519
+ return entity ? permissionsManager.toSubject(entity, model) : model;
3520
+ };
3370
3521
  const can = (action, entity, field) => {
3371
- return userAbility.can(action, toSubject(entity), field);
3522
+ const subject = toSubject(entity);
3523
+ const aliases = actionProvider.unstable_aliases(action, model);
3524
+ return (
3525
+ // Test the original action to see if it passes
3526
+ userAbility.can(action, subject, field) || // Else try every known alias if at least one of them succeed, then the user "can"
3527
+ aliases.some((alias) => userAbility.can(alias, subject, field))
3528
+ );
3372
3529
  };
3373
3530
  const cannot = (action, entity, field) => {
3374
- return userAbility.cannot(action, toSubject(entity), field);
3531
+ const subject = toSubject(entity);
3532
+ const aliases = actionProvider.unstable_aliases(action, model);
3533
+ return (
3534
+ // Test both the original action
3535
+ userAbility.cannot(action, subject, field) && // and every known alias, if all of them fail (cannot), then the user truly "cannot"
3536
+ aliases.every((alias) => userAbility.cannot(alias, subject, field))
3537
+ );
3375
3538
  };
3376
3539
  const sanitizeOutput = (data, { action = ACTIONS.read } = {}) => {
3377
3540
  return permissionsManager.sanitizeOutput(data, { subject: toSubject(data), action });
@@ -3514,7 +3677,7 @@ const permission = ({ strapi: strapi2 }) => ({
3514
3677
  await strapi2.service("admin::permission").actionProvider.registerMany(actions);
3515
3678
  }
3516
3679
  });
3517
- const { isVisibleAttribute: isVisibleAttribute$1 } = strapiUtils__default.default.contentTypes;
3680
+ const { isVisibleAttribute: isVisibleAttribute$1, isScalarAttribute, getDoesAttributeRequireValidation } = strapiUtils__default.default.contentTypes;
3518
3681
  const { isAnyToMany } = strapiUtils__default.default.relations;
3519
3682
  const { PUBLISHED_AT_ATTRIBUTE: PUBLISHED_AT_ATTRIBUTE$1 } = strapiUtils__default.default.contentTypes.constants;
3520
3683
  const isMorphToRelation = (attribute) => isRelation(attribute) && attribute.relation.includes("morphTo");
@@ -3605,6 +3768,42 @@ const getDeepPopulate = (uid2, {
3605
3768
  {}
3606
3769
  );
3607
3770
  };
3771
+ const getValidatableFieldsPopulate = (uid2, {
3772
+ initialPopulate = {},
3773
+ countMany = false,
3774
+ countOne = false,
3775
+ maxLevel = Infinity
3776
+ } = {}, level = 1) => {
3777
+ if (level > maxLevel) {
3778
+ return {};
3779
+ }
3780
+ const model = strapi.getModel(uid2);
3781
+ return Object.entries(model.attributes).reduce((populateAcc, [attributeName, attribute]) => {
3782
+ if (!getDoesAttributeRequireValidation(attribute)) {
3783
+ return populateAcc;
3784
+ }
3785
+ if (isScalarAttribute(attribute)) {
3786
+ return fp.merge(populateAcc, {
3787
+ [attributeName]: true
3788
+ });
3789
+ }
3790
+ return fp.merge(
3791
+ populateAcc,
3792
+ getPopulateFor(
3793
+ attributeName,
3794
+ model,
3795
+ {
3796
+ // @ts-expect-error - improve types
3797
+ initialPopulate: initialPopulate?.[attributeName],
3798
+ countMany,
3799
+ countOne,
3800
+ maxLevel
3801
+ },
3802
+ level
3803
+ )
3804
+ );
3805
+ }, {});
3806
+ };
3608
3807
  const getDeepPopulateDraftCount = (uid2) => {
3609
3808
  const model = strapi.getModel(uid2);
3610
3809
  let hasRelations = false;
@@ -3612,6 +3811,10 @@ const getDeepPopulateDraftCount = (uid2) => {
3612
3811
  const attribute = model.attributes[attributeName];
3613
3812
  switch (attribute.type) {
3614
3813
  case "relation": {
3814
+ const isMorphRelation = attribute.relation.toLowerCase().startsWith("morph");
3815
+ if (isMorphRelation) {
3816
+ break;
3817
+ }
3615
3818
  if (isVisibleAttribute$1(model, attributeName)) {
3616
3819
  populateAcc[attributeName] = {
3617
3820
  count: true,
@@ -3626,22 +3829,24 @@ const getDeepPopulateDraftCount = (uid2) => {
3626
3829
  attribute.component
3627
3830
  );
3628
3831
  if (childHasRelations) {
3629
- populateAcc[attributeName] = { populate: populate2 };
3832
+ populateAcc[attributeName] = {
3833
+ populate: populate2
3834
+ };
3630
3835
  hasRelations = true;
3631
3836
  }
3632
3837
  break;
3633
3838
  }
3634
3839
  case "dynamiczone": {
3635
- const dzPopulate = (attribute.components || []).reduce((acc, componentUID) => {
3636
- const { populate: populate2, hasRelations: childHasRelations } = getDeepPopulateDraftCount(componentUID);
3637
- if (childHasRelations) {
3840
+ const dzPopulateFragment = attribute.components?.reduce((acc, componentUID) => {
3841
+ const { populate: componentPopulate, hasRelations: componentHasRelations } = getDeepPopulateDraftCount(componentUID);
3842
+ if (componentHasRelations) {
3638
3843
  hasRelations = true;
3639
- return fp.merge(acc, populate2);
3844
+ return { ...acc, [componentUID]: { populate: componentPopulate } };
3640
3845
  }
3641
3846
  return acc;
3642
3847
  }, {});
3643
- if (!fp.isEmpty(dzPopulate)) {
3644
- populateAcc[attributeName] = { populate: dzPopulate };
3848
+ if (!fp.isEmpty(dzPopulateFragment)) {
3849
+ populateAcc[attributeName] = { on: dzPopulateFragment };
3645
3850
  }
3646
3851
  break;
3647
3852
  }
@@ -3833,41 +4038,70 @@ const AVAILABLE_STATUS_FIELDS = [
3833
4038
  "updatedBy",
3834
4039
  "status"
3835
4040
  ];
3836
- const AVAILABLE_LOCALES_FIELDS = ["id", "locale", "updatedAt", "createdAt", "status"];
4041
+ const AVAILABLE_LOCALES_FIELDS = [
4042
+ "id",
4043
+ "locale",
4044
+ "updatedAt",
4045
+ "createdAt",
4046
+ "status",
4047
+ "publishedAt",
4048
+ "documentId"
4049
+ ];
3837
4050
  const CONTENT_MANAGER_STATUS = {
3838
4051
  PUBLISHED: "published",
3839
4052
  DRAFT: "draft",
3840
4053
  MODIFIED: "modified"
3841
4054
  };
3842
- const areDatesEqual = (date1, date2, threshold) => {
3843
- if (!date1 || !date2) {
4055
+ const getIsVersionLatestModification = (version, otherVersion) => {
4056
+ if (!version || !version.updatedAt) {
3844
4057
  return false;
3845
4058
  }
3846
- const time1 = new Date(date1).getTime();
3847
- const time2 = new Date(date2).getTime();
3848
- const difference = Math.abs(time1 - time2);
3849
- return difference <= threshold;
4059
+ const versionUpdatedAt = version?.updatedAt ? new Date(version.updatedAt).getTime() : 0;
4060
+ const otherUpdatedAt = otherVersion?.updatedAt ? new Date(otherVersion.updatedAt).getTime() : 0;
4061
+ return versionUpdatedAt > otherUpdatedAt;
3850
4062
  };
3851
4063
  const documentMetadata = ({ strapi: strapi2 }) => ({
3852
4064
  /**
3853
4065
  * Returns available locales of a document for the current status
3854
4066
  */
3855
- getAvailableLocales(uid2, version, allVersions) {
4067
+ async getAvailableLocales(uid2, version, allVersions, validatableFields = []) {
3856
4068
  const versionsByLocale = fp.groupBy("locale", allVersions);
3857
4069
  delete versionsByLocale[version.locale];
3858
- return Object.values(versionsByLocale).map((localeVersions) => {
3859
- if (!strapiUtils.contentTypes.hasDraftAndPublish(strapi2.getModel(uid2))) {
3860
- return fp.pick(AVAILABLE_LOCALES_FIELDS, localeVersions[0]);
4070
+ const model = strapi2.getModel(uid2);
4071
+ const keysToKeep = [...AVAILABLE_LOCALES_FIELDS, ...validatableFields];
4072
+ const traversalFunction = async (localeVersion) => strapiUtils.traverseEntity(
4073
+ ({ key }, { remove }) => {
4074
+ if (keysToKeep.includes(key)) {
4075
+ return;
4076
+ }
4077
+ remove(key);
4078
+ },
4079
+ { schema: model, getModel: strapi2.getModel.bind(strapi2) },
4080
+ // @ts-expect-error fix types DocumentVersion incompatible with Data
4081
+ localeVersion
4082
+ );
4083
+ const mappingResult = await strapiUtils.async.map(
4084
+ Object.values(versionsByLocale),
4085
+ async (localeVersions) => {
4086
+ const mappedLocaleVersions = await strapiUtils.async.map(
4087
+ localeVersions,
4088
+ traversalFunction
4089
+ );
4090
+ if (!strapiUtils.contentTypes.hasDraftAndPublish(model)) {
4091
+ return mappedLocaleVersions[0];
4092
+ }
4093
+ const draftVersion = mappedLocaleVersions.find((v) => v.publishedAt === null);
4094
+ const otherVersions = mappedLocaleVersions.filter((v) => v.id !== draftVersion?.id);
4095
+ if (!draftVersion) {
4096
+ return;
4097
+ }
4098
+ return {
4099
+ ...draftVersion,
4100
+ status: this.getStatus(draftVersion, otherVersions)
4101
+ };
3861
4102
  }
3862
- const draftVersion = localeVersions.find((v) => v.publishedAt === null);
3863
- const otherVersions = localeVersions.filter((v) => v.id !== draftVersion?.id);
3864
- if (!draftVersion)
3865
- return;
3866
- return {
3867
- ...fp.pick(AVAILABLE_LOCALES_FIELDS, draftVersion),
3868
- status: this.getStatus(draftVersion, otherVersions)
3869
- };
3870
- }).filter(Boolean);
4103
+ );
4104
+ return mappingResult.filter(Boolean);
3871
4105
  },
3872
4106
  /**
3873
4107
  * Returns available status of a document for the current locale
@@ -3905,26 +4139,37 @@ const documentMetadata = ({ strapi: strapi2 }) => ({
3905
4139
  });
3906
4140
  },
3907
4141
  getStatus(version, otherDocumentStatuses) {
3908
- const isDraft = version.publishedAt === null;
3909
- if (!otherDocumentStatuses?.length) {
3910
- return isDraft ? CONTENT_MANAGER_STATUS.DRAFT : CONTENT_MANAGER_STATUS.PUBLISHED;
4142
+ let draftVersion;
4143
+ let publishedVersion;
4144
+ if (version.publishedAt) {
4145
+ publishedVersion = version;
4146
+ } else {
4147
+ draftVersion = version;
3911
4148
  }
3912
- if (isDraft) {
3913
- const publishedVersion = otherDocumentStatuses?.find((d) => d.publishedAt !== null);
3914
- if (!publishedVersion) {
3915
- return CONTENT_MANAGER_STATUS.DRAFT;
3916
- }
4149
+ const otherVersion = otherDocumentStatuses?.at(0);
4150
+ if (otherVersion?.publishedAt) {
4151
+ publishedVersion = otherVersion;
4152
+ } else if (otherVersion) {
4153
+ draftVersion = otherVersion;
3917
4154
  }
3918
- if (areDatesEqual(version.updatedAt, otherDocumentStatuses.at(0)?.updatedAt, 500)) {
4155
+ if (!draftVersion)
3919
4156
  return CONTENT_MANAGER_STATUS.PUBLISHED;
3920
- }
3921
- return CONTENT_MANAGER_STATUS.MODIFIED;
4157
+ if (!publishedVersion)
4158
+ return CONTENT_MANAGER_STATUS.DRAFT;
4159
+ const isDraftModified = getIsVersionLatestModification(draftVersion, publishedVersion);
4160
+ return isDraftModified ? CONTENT_MANAGER_STATUS.MODIFIED : CONTENT_MANAGER_STATUS.PUBLISHED;
3922
4161
  },
4162
+ // TODO is it necessary to return metadata on every page of the CM
4163
+ // We could refactor this so the locales are only loaded when they're
4164
+ // needed. e.g. in the bulk locale action modal.
3923
4165
  async getMetadata(uid2, version, { availableLocales = true, availableStatus = true } = {}) {
4166
+ const populate = getValidatableFieldsPopulate(uid2);
3924
4167
  const versions = await strapi2.db.query(uid2).findMany({
3925
4168
  where: { documentId: version.documentId },
3926
- select: ["createdAt", "updatedAt", "locale", "publishedAt", "documentId"],
3927
4169
  populate: {
4170
+ // Populate only fields that require validation for bulk locale actions
4171
+ ...populate,
4172
+ // NOTE: creator fields are selected in this way to avoid exposing sensitive data
3928
4173
  createdBy: {
3929
4174
  select: ["id", "firstname", "lastname", "email"]
3930
4175
  },
@@ -3933,7 +4178,7 @@ const documentMetadata = ({ strapi: strapi2 }) => ({
3933
4178
  }
3934
4179
  }
3935
4180
  });
3936
- const availableLocalesResult = availableLocales ? this.getAvailableLocales(uid2, version, versions) : [];
4181
+ const availableLocalesResult = availableLocales ? await this.getAvailableLocales(uid2, version, versions, Object.keys(populate)) : [];
3937
4182
  const availableStatusResult = availableStatus ? this.getAvailableStatus(version, versions) : null;
3938
4183
  return {
3939
4184
  availableLocales: availableLocalesResult,
@@ -3946,8 +4191,15 @@ const documentMetadata = ({ strapi: strapi2 }) => ({
3946
4191
  * - Available status of the document for the current locale
3947
4192
  */
3948
4193
  async formatDocumentWithMetadata(uid2, document, opts = {}) {
3949
- if (!document)
3950
- return document;
4194
+ if (!document) {
4195
+ return {
4196
+ data: document,
4197
+ meta: {
4198
+ availableLocales: [],
4199
+ availableStatus: []
4200
+ }
4201
+ };
4202
+ }
3951
4203
  const hasDraftAndPublish = strapiUtils.contentTypes.hasDraftAndPublish(strapi2.getModel(uid2));
3952
4204
  if (!hasDraftAndPublish) {
3953
4205
  opts.availableStatus = false;
@@ -3997,26 +4249,9 @@ const sumDraftCounts = (entity, uid2) => {
3997
4249
  }, 0);
3998
4250
  };
3999
4251
  const { ApplicationError } = strapiUtils.errors;
4000
- const { ENTRY_PUBLISH, ENTRY_UNPUBLISH } = ALLOWED_WEBHOOK_EVENTS;
4001
4252
  const { PUBLISHED_AT_ATTRIBUTE } = strapiUtils.contentTypes.constants;
4002
4253
  const omitPublishedAtField = fp.omit(PUBLISHED_AT_ATTRIBUTE);
4003
4254
  const omitIdField = fp.omit("id");
4004
- const emitEvent = async (uid2, event, document) => {
4005
- const modelDef = strapi.getModel(uid2);
4006
- const sanitizedDocument = await strapiUtils.sanitize.sanitizers.defaultSanitizeOutput(
4007
- {
4008
- schema: modelDef,
4009
- getModel(uid22) {
4010
- return strapi.getModel(uid22);
4011
- }
4012
- },
4013
- document
4014
- );
4015
- strapi.eventHub.emit(event, {
4016
- model: modelDef.modelName,
4017
- entry: sanitizedDocument
4018
- });
4019
- };
4020
4255
  const documentManager = ({ strapi: strapi2 }) => {
4021
4256
  return {
4022
4257
  async findOne(id, uid2, opts = {}) {
@@ -4035,6 +4270,9 @@ const documentManager = ({ strapi: strapi2 }) => {
4035
4270
  } else if (opts.locale && opts.locale !== "*") {
4036
4271
  where.locale = opts.locale;
4037
4272
  }
4273
+ if (typeof opts.isPublished === "boolean") {
4274
+ where.publishedAt = { $notNull: opts.isPublished };
4275
+ }
4038
4276
  return strapi2.db.query(uid2).findMany({ populate: opts.populate, where });
4039
4277
  },
4040
4278
  async findMany(opts, uid2) {
@@ -4042,20 +4280,16 @@ const documentManager = ({ strapi: strapi2 }) => {
4042
4280
  return strapi2.documents(uid2).findMany(params);
4043
4281
  },
4044
4282
  async findPage(opts, uid2) {
4045
- const page = Number(opts?.page) || 1;
4046
- const pageSize = Number(opts?.pageSize) || 10;
4283
+ const params = strapiUtils.pagination.withDefaultPagination(opts || {}, {
4284
+ maxLimit: 1e3
4285
+ });
4047
4286
  const [documents, total = 0] = await Promise.all([
4048
- strapi2.documents(uid2).findMany(opts),
4049
- strapi2.documents(uid2).count(opts)
4287
+ strapi2.documents(uid2).findMany(params),
4288
+ strapi2.documents(uid2).count(params)
4050
4289
  ]);
4051
4290
  return {
4052
4291
  results: documents,
4053
- pagination: {
4054
- page,
4055
- pageSize,
4056
- pageCount: Math.ceil(total / pageSize),
4057
- total
4058
- }
4292
+ pagination: strapiUtils.pagination.transformPagedPaginationInfo(params, total)
4059
4293
  };
4060
4294
  },
4061
4295
  async create(uid2, opts = {}) {
@@ -4072,10 +4306,7 @@ const documentManager = ({ strapi: strapi2 }) => {
4072
4306
  async clone(id, body, uid2) {
4073
4307
  const populate = await buildDeepPopulate(uid2);
4074
4308
  const params = {
4075
- data: {
4076
- ...omitIdField(body),
4077
- [PUBLISHED_AT_ATTRIBUTE]: null
4078
- },
4309
+ data: omitIdField(body),
4079
4310
  populate
4080
4311
  };
4081
4312
  return strapi2.documents(uid2).clone({ ...params, documentId: id }).then((result) => result?.entries.at(0));
@@ -4101,70 +4332,36 @@ const documentManager = ({ strapi: strapi2 }) => {
4101
4332
  return {};
4102
4333
  },
4103
4334
  // FIXME: handle relations
4104
- async deleteMany(opts, uid2) {
4105
- const docs = await strapi2.documents(uid2).findMany(opts);
4106
- for (const doc of docs) {
4107
- await strapi2.documents(uid2).delete({ documentId: doc.documentId });
4108
- }
4109
- return { count: docs.length };
4335
+ async deleteMany(documentIds, uid2, opts = {}) {
4336
+ const deletedEntries = await strapi2.db.transaction(async () => {
4337
+ return Promise.all(documentIds.map(async (id) => this.delete(id, uid2, opts)));
4338
+ });
4339
+ return { count: deletedEntries.length };
4110
4340
  },
4111
4341
  async publish(id, uid2, opts = {}) {
4112
4342
  const populate = await buildDeepPopulate(uid2);
4113
4343
  const params = { ...opts, populate };
4114
- return strapi2.documents(uid2).publish({ ...params, documentId: id }).then((result) => result?.entries.at(0));
4344
+ return strapi2.documents(uid2).publish({ ...params, documentId: id }).then((result) => result?.entries);
4115
4345
  },
4116
- async publishMany(entities, uid2) {
4117
- if (!entities.length) {
4118
- return null;
4119
- }
4120
- await Promise.all(
4121
- entities.map((document) => {
4122
- return strapi2.entityValidator.validateEntityCreation(
4123
- strapi2.getModel(uid2),
4124
- document,
4125
- void 0,
4126
- // @ts-expect-error - FIXME: entity here is unnecessary
4127
- document
4128
- );
4129
- })
4130
- );
4131
- const entitiesToPublish = entities.filter((doc) => !doc[PUBLISHED_AT_ATTRIBUTE]).map((doc) => doc.id);
4132
- const filters = { id: { $in: entitiesToPublish } };
4133
- const data = { [PUBLISHED_AT_ATTRIBUTE]: /* @__PURE__ */ new Date() };
4134
- const populate = await buildDeepPopulate(uid2);
4135
- const publishedEntitiesCount = await strapi2.db.query(uid2).updateMany({
4136
- where: filters,
4137
- data
4138
- });
4139
- const publishedEntities = await strapi2.db.query(uid2).findMany({
4140
- where: filters,
4141
- populate
4346
+ async publishMany(uid2, documentIds, locale) {
4347
+ return strapi2.db.transaction(async () => {
4348
+ const results = await Promise.all(
4349
+ documentIds.map((documentId) => this.publish(documentId, uid2, { locale }))
4350
+ );
4351
+ const publishedEntitiesCount = results.flat().filter(Boolean).length;
4352
+ return publishedEntitiesCount;
4142
4353
  });
4143
- await Promise.all(
4144
- publishedEntities.map((doc) => emitEvent(uid2, ENTRY_PUBLISH, doc))
4145
- );
4146
- return publishedEntitiesCount;
4147
4354
  },
4148
- async unpublishMany(documents, uid2) {
4149
- if (!documents.length) {
4150
- return null;
4151
- }
4152
- const entitiesToUnpublish = documents.filter((doc) => doc[PUBLISHED_AT_ATTRIBUTE]).map((doc) => doc.id);
4153
- const filters = { id: { $in: entitiesToUnpublish } };
4154
- const data = { [PUBLISHED_AT_ATTRIBUTE]: null };
4155
- const populate = await buildDeepPopulate(uid2);
4156
- const unpublishedEntitiesCount = await strapi2.db.query(uid2).updateMany({
4157
- where: filters,
4158
- data
4159
- });
4160
- const unpublishedEntities = await strapi2.db.query(uid2).findMany({
4161
- where: filters,
4162
- populate
4355
+ async unpublishMany(documentIds, uid2, opts = {}) {
4356
+ const unpublishedEntries = await strapi2.db.transaction(async () => {
4357
+ return Promise.all(
4358
+ documentIds.map(
4359
+ (id) => strapi2.documents(uid2).unpublish({ ...opts, documentId: id }).then((result) => result?.entries)
4360
+ )
4361
+ );
4163
4362
  });
4164
- await Promise.all(
4165
- unpublishedEntities.map((doc) => emitEvent(uid2, ENTRY_UNPUBLISH, doc))
4166
- );
4167
- return unpublishedEntitiesCount;
4363
+ const unpublishedEntitiesCount = unpublishedEntries.flat().filter(Boolean).length;
4364
+ return { count: unpublishedEntitiesCount };
4168
4365
  },
4169
4366
  async unpublish(id, uid2, opts = {}) {
4170
4367
  const populate = await buildDeepPopulate(uid2);
@@ -4189,16 +4386,20 @@ const documentManager = ({ strapi: strapi2 }) => {
4189
4386
  }
4190
4387
  return sumDraftCounts(document, uid2);
4191
4388
  },
4192
- async countManyEntriesDraftRelations(ids, uid2, locale) {
4389
+ async countManyEntriesDraftRelations(documentIds, uid2, locale) {
4193
4390
  const { populate, hasRelations } = getDeepPopulateDraftCount(uid2);
4194
4391
  if (!hasRelations) {
4195
4392
  return 0;
4196
4393
  }
4394
+ let localeFilter = {};
4395
+ if (locale) {
4396
+ localeFilter = Array.isArray(locale) ? { locale: { $in: locale } } : { locale };
4397
+ }
4197
4398
  const entities = await strapi2.db.query(uid2).findMany({
4198
4399
  populate,
4199
4400
  where: {
4200
- id: { $in: ids },
4201
- ...locale ? { locale } : {}
4401
+ documentId: { $in: documentIds },
4402
+ ...localeFilter
4202
4403
  }
4203
4404
  });
4204
4405
  const totalNumberDraftRelations = entities.reduce(