@strapi/content-releases 0.0.0-next.836f74517f9a428a4798ed889c3f05057ec6beb1 → 0.0.0-next.837384c065457f44cba22eb6fb56079cc4b04a2b

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 (143) hide show
  1. package/LICENSE +17 -1
  2. package/dist/_chunks/App-Cx72FM22.mjs +1558 -0
  3. package/dist/_chunks/App-Cx72FM22.mjs.map +1 -0
  4. package/dist/_chunks/App-DyjVixKE.js +1578 -0
  5. package/dist/_chunks/App-DyjVixKE.js.map +1 -0
  6. package/dist/_chunks/{PurchaseContentReleases-YhAPgpG9.js → PurchaseContentReleases-BJlgTIuR.js} +9 -8
  7. package/dist/_chunks/PurchaseContentReleases-BJlgTIuR.js.map +1 -0
  8. package/dist/_chunks/{PurchaseContentReleases-Clm0iACO.mjs → PurchaseContentReleases-CIiBmOhI.mjs} +10 -9
  9. package/dist/_chunks/PurchaseContentReleases-CIiBmOhI.mjs.map +1 -0
  10. package/dist/_chunks/ReleasesSettingsPage-9si_53C5.mjs +178 -0
  11. package/dist/_chunks/ReleasesSettingsPage-9si_53C5.mjs.map +1 -0
  12. package/dist/_chunks/ReleasesSettingsPage-FsXF_FuJ.js +178 -0
  13. package/dist/_chunks/ReleasesSettingsPage-FsXF_FuJ.js.map +1 -0
  14. package/dist/_chunks/{en-gcJJ5htG.js → en-BWPPsSH-.js} +28 -4
  15. package/dist/_chunks/en-BWPPsSH-.js.map +1 -0
  16. package/dist/_chunks/{en-WuuhP6Bn.mjs → en-D9Q4YW03.mjs} +28 -4
  17. package/dist/_chunks/en-D9Q4YW03.mjs.map +1 -0
  18. package/dist/_chunks/index-CVj5EFQC.mjs +1386 -0
  19. package/dist/_chunks/index-CVj5EFQC.mjs.map +1 -0
  20. package/dist/_chunks/index-CpTN5TdF.js +1404 -0
  21. package/dist/_chunks/index-CpTN5TdF.js.map +1 -0
  22. package/dist/_chunks/schemas-DBYv9gK8.js +61 -0
  23. package/dist/_chunks/schemas-DBYv9gK8.js.map +1 -0
  24. package/dist/_chunks/schemas-DdA2ic2U.mjs +44 -0
  25. package/dist/_chunks/schemas-DdA2ic2U.mjs.map +1 -0
  26. package/dist/admin/index.js +1 -15
  27. package/dist/admin/index.js.map +1 -1
  28. package/dist/admin/index.mjs +2 -16
  29. package/dist/admin/index.mjs.map +1 -1
  30. package/dist/admin/src/components/EntryValidationPopover.d.ts +13 -0
  31. package/dist/admin/src/components/RelativeTime.d.ts +28 -0
  32. package/dist/admin/src/components/ReleaseAction.d.ts +3 -0
  33. package/dist/admin/src/components/ReleaseActionMenu.d.ts +26 -0
  34. package/dist/admin/src/components/ReleaseActionModal.d.ts +24 -0
  35. package/dist/admin/src/components/ReleaseActionOptions.d.ts +9 -0
  36. package/dist/admin/src/components/ReleaseListCell.d.ts +28 -0
  37. package/dist/admin/src/components/ReleaseModal.d.ts +17 -0
  38. package/dist/admin/src/components/ReleasesPanel.d.ts +3 -0
  39. package/dist/admin/src/constants.d.ts +76 -0
  40. package/dist/admin/src/index.d.ts +3 -0
  41. package/dist/admin/src/modules/hooks.d.ts +7 -0
  42. package/dist/admin/src/pages/App.d.ts +1 -0
  43. package/dist/admin/src/pages/PurchaseContentReleases.d.ts +2 -0
  44. package/dist/admin/src/pages/ReleaseDetailsPage.d.ts +2 -0
  45. package/dist/admin/src/pages/ReleasesPage.d.ts +8 -0
  46. package/dist/admin/src/pages/ReleasesSettingsPage.d.ts +1 -0
  47. package/dist/admin/src/pages/tests/mockReleaseDetailsPageData.d.ts +181 -0
  48. package/dist/admin/src/pages/tests/mockReleasesPageData.d.ts +39 -0
  49. package/dist/admin/src/pluginId.d.ts +1 -0
  50. package/dist/admin/src/services/release.d.ts +112 -0
  51. package/dist/admin/src/store/hooks.d.ts +7 -0
  52. package/dist/admin/src/utils/api.d.ts +6 -0
  53. package/dist/admin/src/utils/prefixPluginTranslations.d.ts +3 -0
  54. package/dist/admin/src/utils/time.d.ts +10 -0
  55. package/dist/admin/src/validation/schemas.d.ts +6 -0
  56. package/dist/server/index.js +1069 -665
  57. package/dist/server/index.js.map +1 -1
  58. package/dist/server/index.mjs +1069 -663
  59. package/dist/server/index.mjs.map +1 -1
  60. package/dist/server/src/bootstrap.d.ts +5 -0
  61. package/dist/server/src/bootstrap.d.ts.map +1 -0
  62. package/dist/server/src/constants.d.ts +21 -0
  63. package/dist/server/src/constants.d.ts.map +1 -0
  64. package/dist/server/src/content-types/index.d.ts +97 -0
  65. package/dist/server/src/content-types/index.d.ts.map +1 -0
  66. package/dist/server/src/content-types/release/index.d.ts +48 -0
  67. package/dist/server/src/content-types/release/index.d.ts.map +1 -0
  68. package/dist/server/src/content-types/release/schema.d.ts +47 -0
  69. package/dist/server/src/content-types/release/schema.d.ts.map +1 -0
  70. package/dist/server/src/content-types/release-action/index.d.ts +48 -0
  71. package/dist/server/src/content-types/release-action/index.d.ts.map +1 -0
  72. package/dist/server/src/content-types/release-action/schema.d.ts +47 -0
  73. package/dist/server/src/content-types/release-action/schema.d.ts.map +1 -0
  74. package/dist/server/src/controllers/index.d.ts +25 -0
  75. package/dist/server/src/controllers/index.d.ts.map +1 -0
  76. package/dist/server/src/controllers/release-action.d.ts +10 -0
  77. package/dist/server/src/controllers/release-action.d.ts.map +1 -0
  78. package/dist/server/src/controllers/release.d.ts +18 -0
  79. package/dist/server/src/controllers/release.d.ts.map +1 -0
  80. package/dist/server/src/controllers/settings.d.ts +11 -0
  81. package/dist/server/src/controllers/settings.d.ts.map +1 -0
  82. package/dist/server/src/controllers/validation/release-action.d.ts +14 -0
  83. package/dist/server/src/controllers/validation/release-action.d.ts.map +1 -0
  84. package/dist/server/src/controllers/validation/release.d.ts +4 -0
  85. package/dist/server/src/controllers/validation/release.d.ts.map +1 -0
  86. package/dist/server/src/controllers/validation/settings.d.ts +3 -0
  87. package/dist/server/src/controllers/validation/settings.d.ts.map +1 -0
  88. package/dist/server/src/destroy.d.ts +5 -0
  89. package/dist/server/src/destroy.d.ts.map +1 -0
  90. package/dist/server/src/index.d.ts +2111 -0
  91. package/dist/server/src/index.d.ts.map +1 -0
  92. package/dist/server/src/middlewares/documents.d.ts +6 -0
  93. package/dist/server/src/middlewares/documents.d.ts.map +1 -0
  94. package/dist/server/src/migrations/database/5.0.0-document-id-in-actions.d.ts +9 -0
  95. package/dist/server/src/migrations/database/5.0.0-document-id-in-actions.d.ts.map +1 -0
  96. package/dist/server/src/migrations/index.d.ts +13 -0
  97. package/dist/server/src/migrations/index.d.ts.map +1 -0
  98. package/dist/server/src/register.d.ts +5 -0
  99. package/dist/server/src/register.d.ts.map +1 -0
  100. package/dist/server/src/routes/index.d.ts +51 -0
  101. package/dist/server/src/routes/index.d.ts.map +1 -0
  102. package/dist/server/src/routes/release-action.d.ts +18 -0
  103. package/dist/server/src/routes/release-action.d.ts.map +1 -0
  104. package/dist/server/src/routes/release.d.ts +18 -0
  105. package/dist/server/src/routes/release.d.ts.map +1 -0
  106. package/dist/server/src/routes/settings.d.ts +18 -0
  107. package/dist/server/src/routes/settings.d.ts.map +1 -0
  108. package/dist/server/src/services/index.d.ts +1824 -0
  109. package/dist/server/src/services/index.d.ts.map +1 -0
  110. package/dist/server/src/services/release-action.d.ts +34 -0
  111. package/dist/server/src/services/release-action.d.ts.map +1 -0
  112. package/dist/server/src/services/release.d.ts +31 -0
  113. package/dist/server/src/services/release.d.ts.map +1 -0
  114. package/dist/server/src/services/scheduling.d.ts +18 -0
  115. package/dist/server/src/services/scheduling.d.ts.map +1 -0
  116. package/dist/server/src/services/settings.d.ts +13 -0
  117. package/dist/server/src/services/settings.d.ts.map +1 -0
  118. package/dist/server/src/services/validation.d.ts +18 -0
  119. package/dist/server/src/services/validation.d.ts.map +1 -0
  120. package/dist/server/src/utils/index.d.ts +35 -0
  121. package/dist/server/src/utils/index.d.ts.map +1 -0
  122. package/dist/shared/contracts/release-actions.d.ts +137 -0
  123. package/dist/shared/contracts/release-actions.d.ts.map +1 -0
  124. package/dist/shared/contracts/releases.d.ts +184 -0
  125. package/dist/shared/contracts/releases.d.ts.map +1 -0
  126. package/dist/shared/contracts/settings.d.ts +39 -0
  127. package/dist/shared/contracts/settings.d.ts.map +1 -0
  128. package/dist/shared/types.d.ts +24 -0
  129. package/dist/shared/types.d.ts.map +1 -0
  130. package/package.json +35 -39
  131. package/dist/_chunks/App-fcvNs2Qb.mjs +0 -1322
  132. package/dist/_chunks/App-fcvNs2Qb.mjs.map +0 -1
  133. package/dist/_chunks/App-pNsURCL_.js +0 -1345
  134. package/dist/_chunks/App-pNsURCL_.js.map +0 -1
  135. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs.map +0 -1
  136. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js.map +0 -1
  137. package/dist/_chunks/en-WuuhP6Bn.mjs.map +0 -1
  138. package/dist/_chunks/en-gcJJ5htG.js.map +0 -1
  139. package/dist/_chunks/index-gzTuOXiK.js +0 -1034
  140. package/dist/_chunks/index-gzTuOXiK.js.map +0 -1
  141. package/dist/_chunks/index-pxhi8wsT.mjs +0 -1013
  142. package/dist/_chunks/index-pxhi8wsT.mjs.map +0 -1
  143. package/strapi-server.js +0 -3
@@ -3,13 +3,11 @@ const utils = require("@strapi/utils");
3
3
  const isEqual = require("lodash/isEqual");
4
4
  const lodash = require("lodash");
5
5
  const _ = require("lodash/fp");
6
- const EE = require("@strapi/strapi/dist/utils/ee");
7
6
  const nodeSchedule = require("node-schedule");
8
7
  const yup = require("yup");
9
8
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
10
9
  function _interopNamespace(e) {
11
- if (e && e.__esModule)
12
- return e;
10
+ if (e && e.__esModule) return e;
13
11
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
14
12
  if (e) {
15
13
  for (const k in e) {
@@ -27,7 +25,6 @@ function _interopNamespace(e) {
27
25
  }
28
26
  const isEqual__default = /* @__PURE__ */ _interopDefault(isEqual);
29
27
  const ___default = /* @__PURE__ */ _interopDefault(_);
30
- const EE__default = /* @__PURE__ */ _interopDefault(EE);
31
28
  const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
32
29
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
33
30
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -73,21 +70,38 @@ const ACTIONS = [
73
70
  displayName: "Add an entry to a release",
74
71
  uid: "create-action",
75
72
  pluginName: "content-releases"
73
+ },
74
+ // Settings
75
+ {
76
+ uid: "settings.read",
77
+ section: "settings",
78
+ displayName: "Read",
79
+ category: "content releases",
80
+ subCategory: "options",
81
+ pluginName: "content-releases"
82
+ },
83
+ {
84
+ uid: "settings.update",
85
+ section: "settings",
86
+ displayName: "Edit",
87
+ category: "content releases",
88
+ subCategory: "options",
89
+ pluginName: "content-releases"
76
90
  }
77
91
  ];
78
92
  const ALLOWED_WEBHOOK_EVENTS = {
79
93
  RELEASES_PUBLISH: "releases.publish"
80
94
  };
81
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
95
+ const getService = (name, { strapi: strapi2 }) => {
82
96
  return strapi2.plugin("content-releases").service(name);
83
97
  };
84
- const getPopulatedEntry = async (contentTypeUid, entryId, { strapi: strapi2 } = { strapi: global.strapi }) => {
98
+ const getDraftEntryValidStatus = async ({ contentType, documentId, locale }, { strapi: strapi2 }) => {
85
99
  const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
86
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
87
- const entry = await strapi2.entityService.findOne(contentTypeUid, entryId, { populate });
88
- return entry;
100
+ const populate = await populateBuilderService(contentType).populateDeep(Infinity).build();
101
+ const entry = await getEntry({ contentType, documentId, locale, populate }, { strapi: strapi2 });
102
+ return isEntryValid(contentType, entry, { strapi: strapi2 });
89
103
  };
90
- const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } = { strapi: global.strapi }) => {
104
+ const isEntryValid = async (contentTypeUid, entry, { strapi: strapi2 }) => {
91
105
  try {
92
106
  await strapi2.entityValidator.validateEntityCreation(
93
107
  strapi2.getModel(contentTypeUid),
@@ -96,11 +110,54 @@ const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } =
96
110
  // @ts-expect-error - FIXME: entity here is unnecessary
97
111
  entry
98
112
  );
113
+ const workflowsService = strapi2.plugin("review-workflows").service("workflows");
114
+ const workflow = await workflowsService.getAssignedWorkflow(contentTypeUid, {
115
+ populate: "stageRequiredToPublish"
116
+ });
117
+ if (workflow?.stageRequiredToPublish) {
118
+ return entry.strapi_stage.id === workflow.stageRequiredToPublish.id;
119
+ }
99
120
  return true;
100
121
  } catch {
101
122
  return false;
102
123
  }
103
124
  };
125
+ const getEntry = async ({
126
+ contentType,
127
+ documentId,
128
+ locale,
129
+ populate,
130
+ status = "draft"
131
+ }, { strapi: strapi2 }) => {
132
+ if (documentId) {
133
+ const entry = await strapi2.documents(contentType).findOne({ documentId, locale, populate, status });
134
+ if (status === "published" && !entry) {
135
+ return strapi2.documents(contentType).findOne({ documentId, locale, populate, status: "draft" });
136
+ }
137
+ return entry;
138
+ }
139
+ return strapi2.documents(contentType).findFirst({ locale, populate, status });
140
+ };
141
+ const getEntryStatus = async (contentType, entry) => {
142
+ if (entry.publishedAt) {
143
+ return "published";
144
+ }
145
+ const publishedEntry = await strapi.documents(contentType).findOne({
146
+ documentId: entry.documentId,
147
+ locale: entry.locale,
148
+ status: "published",
149
+ fields: ["updatedAt"]
150
+ });
151
+ if (!publishedEntry) {
152
+ return "draft";
153
+ }
154
+ const entryUpdatedAt = new Date(entry.updatedAt).getTime();
155
+ const publishedEntryUpdatedAt = new Date(publishedEntry.updatedAt).getTime();
156
+ if (entryUpdatedAt > publishedEntryUpdatedAt) {
157
+ return "modified";
158
+ }
159
+ return "published";
160
+ };
104
161
  async function deleteActionsOnDisableDraftAndPublish({
105
162
  oldContentTypes,
106
163
  contentTypes: contentTypes2
@@ -122,7 +179,7 @@ async function deleteActionsOnDisableDraftAndPublish({
122
179
  async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
123
180
  const deletedContentTypes = lodash.difference(lodash.keys(oldContentTypes), lodash.keys(contentTypes2)) ?? [];
124
181
  if (deletedContentTypes.length) {
125
- await utils.mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
182
+ await utils.async.map(deletedContentTypes, async (deletedContentTypeUID) => {
126
183
  return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
127
184
  });
128
185
  }
@@ -141,25 +198,27 @@ async function migrateIsValidAndStatusReleases() {
141
198
  }
142
199
  }
143
200
  });
144
- utils.mapAsync(releasesWithoutStatus, async (release2) => {
201
+ utils.async.map(releasesWithoutStatus, async (release2) => {
145
202
  const actions = release2.actions;
146
203
  const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
147
204
  for (const action of notValidatedActions) {
148
205
  if (action.entry) {
149
- const populatedEntry = await getPopulatedEntry(action.contentType, action.entry.id, {
150
- strapi
206
+ const isEntryValid2 = getDraftEntryValidStatus(
207
+ {
208
+ contentType: action.contentType,
209
+ documentId: action.entryDocumentId,
210
+ locale: action.locale
211
+ },
212
+ { strapi }
213
+ );
214
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
215
+ where: {
216
+ id: action.id
217
+ },
218
+ data: {
219
+ isEntryValid: isEntryValid2
220
+ }
151
221
  });
152
- if (populatedEntry) {
153
- const isEntryValid = getEntryValidStatus(action.contentType, populatedEntry, { strapi });
154
- await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
155
- where: {
156
- id: action.id
157
- },
158
- data: {
159
- isEntryValid
160
- }
161
- });
162
- }
163
222
  }
164
223
  }
165
224
  return getService("release", { strapi }).updateReleaseStatus(release2.id);
@@ -172,7 +231,7 @@ async function migrateIsValidAndStatusReleases() {
172
231
  }
173
232
  }
174
233
  });
175
- utils.mapAsync(publishedReleases, async (release2) => {
234
+ utils.async.map(publishedReleases, async (release2) => {
176
235
  return strapi.db.query(RELEASE_MODEL_UID).update({
177
236
  where: {
178
237
  id: release2.id
@@ -189,7 +248,7 @@ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: co
189
248
  (uid) => oldContentTypes[uid]?.options?.draftAndPublish
190
249
  );
191
250
  const releasesAffected = /* @__PURE__ */ new Set();
192
- utils.mapAsync(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
251
+ utils.async.map(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
193
252
  const oldContentType = oldContentTypes[contentTypeUID];
194
253
  const contentType = contentTypes2[contentTypeUID];
195
254
  if (!isEqual__default.default(oldContentType?.attributes, contentType?.attributes)) {
@@ -202,30 +261,30 @@ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: co
202
261
  release: true
203
262
  }
204
263
  });
205
- await utils.mapAsync(actions, async (action) => {
206
- if (action.entry && action.release) {
207
- const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
208
- strapi
264
+ await utils.async.map(actions, async (action) => {
265
+ if (action.entry && action.release && action.type === "publish") {
266
+ const isEntryValid2 = await getDraftEntryValidStatus(
267
+ {
268
+ contentType: contentTypeUID,
269
+ documentId: action.entryDocumentId,
270
+ locale: action.locale
271
+ },
272
+ { strapi }
273
+ );
274
+ releasesAffected.add(action.release.id);
275
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
276
+ where: {
277
+ id: action.id
278
+ },
279
+ data: {
280
+ isEntryValid: isEntryValid2
281
+ }
209
282
  });
210
- if (populatedEntry) {
211
- const isEntryValid = await getEntryValidStatus(contentTypeUID, populatedEntry, {
212
- strapi
213
- });
214
- releasesAffected.add(action.release.id);
215
- await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
216
- where: {
217
- id: action.id
218
- },
219
- data: {
220
- isEntryValid
221
- }
222
- });
223
- }
224
283
  }
225
284
  });
226
285
  }
227
286
  }).then(() => {
228
- utils.mapAsync(releasesAffected, async (releaseId) => {
287
+ utils.async.map(releasesAffected, async (releaseId) => {
229
288
  return getService("release", { strapi }).updateReleaseStatus(releaseId);
230
289
  });
231
290
  });
@@ -235,13 +294,16 @@ async function disableContentTypeLocalized({ oldContentTypes, contentTypes: cont
235
294
  if (!oldContentTypes) {
236
295
  return;
237
296
  }
297
+ const i18nPlugin = strapi.plugin("i18n");
298
+ if (!i18nPlugin) {
299
+ return;
300
+ }
238
301
  for (const uid in contentTypes2) {
239
302
  if (!oldContentTypes[uid]) {
240
303
  continue;
241
304
  }
242
305
  const oldContentType = oldContentTypes[uid];
243
306
  const contentType = contentTypes2[uid];
244
- const i18nPlugin = strapi.plugin("i18n");
245
307
  const { isLocalizedContentType } = i18nPlugin.service("content-types");
246
308
  if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
247
309
  await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
@@ -254,13 +316,16 @@ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: conte
254
316
  if (!oldContentTypes) {
255
317
  return;
256
318
  }
319
+ const i18nPlugin = strapi.plugin("i18n");
320
+ if (!i18nPlugin) {
321
+ return;
322
+ }
257
323
  for (const uid in contentTypes2) {
258
324
  if (!oldContentTypes[uid]) {
259
325
  continue;
260
326
  }
261
327
  const oldContentType = oldContentTypes[uid];
262
328
  const contentType = contentTypes2[uid];
263
- const i18nPlugin = strapi.plugin("i18n");
264
329
  const { isLocalizedContentType } = i18nPlugin.service("content-types");
265
330
  const { getDefaultLocale } = i18nPlugin.service("locales");
266
331
  if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
@@ -271,158 +336,197 @@ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: conte
271
336
  }
272
337
  }
273
338
  }
274
- const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
339
+ const addEntryDocumentToReleaseActions = {
340
+ name: "content-releases::5.0.0-add-entry-document-id-to-release-actions",
341
+ async up(trx, db) {
342
+ const hasTable = await trx.schema.hasTable("strapi_release_actions");
343
+ if (!hasTable) {
344
+ return;
345
+ }
346
+ const hasPolymorphicColumn = await trx.schema.hasColumn("strapi_release_actions", "target_id");
347
+ if (hasPolymorphicColumn) {
348
+ const hasEntryDocumentIdColumn = await trx.schema.hasColumn(
349
+ "strapi_release_actions",
350
+ "entry_document_id"
351
+ );
352
+ if (!hasEntryDocumentIdColumn) {
353
+ await trx.schema.alterTable("strapi_release_actions", (table) => {
354
+ table.string("entry_document_id");
355
+ });
356
+ }
357
+ const releaseActions = await trx.select("*").from("strapi_release_actions");
358
+ utils.async.map(releaseActions, async (action) => {
359
+ const { target_type, target_id } = action;
360
+ const entry = await db.query(target_type).findOne({ where: { id: target_id } });
361
+ if (entry) {
362
+ await trx("strapi_release_actions").update({ entry_document_id: entry.documentId }).where("id", action.id);
363
+ }
364
+ });
365
+ }
366
+ },
367
+ async down() {
368
+ throw new Error("not implemented");
369
+ }
370
+ };
275
371
  const register = async ({ strapi: strapi2 }) => {
276
- if (features$2.isEnabled("cms-content-releases")) {
277
- await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
278
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish).register(disableContentTypeLocalized);
372
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
373
+ await strapi2.service("admin::permission").actionProvider.registerMany(ACTIONS);
374
+ strapi2.db.migrations.providers.internal.register(addEntryDocumentToReleaseActions);
375
+ strapi2.hook("strapi::content-types.beforeSync").register(disableContentTypeLocalized).register(deleteActionsOnDisableDraftAndPublish);
279
376
  strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
280
377
  }
378
+ if (strapi2.plugin("graphql")) {
379
+ const graphqlExtensionService = strapi2.plugin("graphql").service("extension");
380
+ graphqlExtensionService.shadowCRUD(RELEASE_MODEL_UID).disable();
381
+ graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
382
+ }
383
+ };
384
+ const updateActionsStatusAndUpdateReleaseStatus = async (contentType, entry) => {
385
+ const releases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
386
+ where: {
387
+ releasedAt: null,
388
+ actions: {
389
+ contentType,
390
+ entryDocumentId: entry.documentId,
391
+ locale: entry.locale
392
+ }
393
+ }
394
+ });
395
+ const entryStatus = await isEntryValid(contentType, entry, { strapi });
396
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).updateMany({
397
+ where: {
398
+ contentType,
399
+ entryDocumentId: entry.documentId,
400
+ locale: entry.locale
401
+ },
402
+ data: {
403
+ isEntryValid: entryStatus
404
+ }
405
+ });
406
+ for (const release2 of releases) {
407
+ getService("release", { strapi }).updateReleaseStatus(release2.id);
408
+ }
409
+ };
410
+ const deleteActionsAndUpdateReleaseStatus = async (params) => {
411
+ const releases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
412
+ where: {
413
+ actions: params
414
+ }
415
+ });
416
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
417
+ where: params
418
+ });
419
+ for (const release2 of releases) {
420
+ getService("release", { strapi }).updateReleaseStatus(release2.id);
421
+ }
422
+ };
423
+ const deleteActionsOnDelete = async (ctx, next) => {
424
+ if (ctx.action !== "delete") {
425
+ return next();
426
+ }
427
+ if (!utils.contentTypes.hasDraftAndPublish(ctx.contentType)) {
428
+ return next();
429
+ }
430
+ const contentType = ctx.contentType.uid;
431
+ const { documentId, locale } = ctx.params;
432
+ const result = await next();
433
+ if (!result) {
434
+ return result;
435
+ }
436
+ try {
437
+ deleteActionsAndUpdateReleaseStatus({
438
+ contentType,
439
+ entryDocumentId: documentId,
440
+ ...locale !== "*" && { locale }
441
+ });
442
+ } catch (error) {
443
+ strapi.log.error("Error while deleting release actions after delete", {
444
+ error
445
+ });
446
+ }
447
+ return result;
448
+ };
449
+ const updateActionsOnUpdate = async (ctx, next) => {
450
+ if (ctx.action !== "update") {
451
+ return next();
452
+ }
453
+ if (!utils.contentTypes.hasDraftAndPublish(ctx.contentType)) {
454
+ return next();
455
+ }
456
+ const contentType = ctx.contentType.uid;
457
+ const result = await next();
458
+ if (!result) {
459
+ return result;
460
+ }
461
+ try {
462
+ updateActionsStatusAndUpdateReleaseStatus(contentType, result);
463
+ } catch (error) {
464
+ strapi.log.error("Error while updating release actions after update", {
465
+ error
466
+ });
467
+ }
468
+ return result;
469
+ };
470
+ const deleteReleasesActionsAndUpdateReleaseStatus = async (params) => {
471
+ const releases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
472
+ where: {
473
+ actions: params
474
+ }
475
+ });
476
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
477
+ where: params
478
+ });
479
+ for (const release2 of releases) {
480
+ getService("release", { strapi }).updateReleaseStatus(release2.id);
481
+ }
281
482
  };
282
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
283
483
  const bootstrap = async ({ strapi: strapi2 }) => {
284
- if (features$1.isEnabled("cms-content-releases")) {
484
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
285
485
  const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
286
486
  (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
287
487
  );
288
488
  strapi2.db.lifecycles.subscribe({
289
489
  models: contentTypesWithDraftAndPublish,
290
- async afterDelete(event) {
291
- try {
292
- const { model, result } = event;
293
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
294
- const { id } = result;
295
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
296
- where: {
297
- actions: {
298
- target_type: model.uid,
299
- target_id: id
300
- }
301
- }
302
- });
303
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
304
- where: {
305
- target_type: model.uid,
306
- target_id: id
307
- }
308
- });
309
- for (const release2 of releases) {
310
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
311
- }
312
- }
313
- } catch (error) {
314
- strapi2.log.error("Error while deleting release actions after entry delete", { error });
315
- }
316
- },
317
490
  /**
318
- * deleteMany hook doesn't return the deleted entries ids
319
- * so we need to fetch them before deleting the entries to save the ids on our state
320
- */
321
- async beforeDeleteMany(event) {
322
- const { model, params } = event;
323
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
324
- const { where } = params;
325
- const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
326
- event.state.entriesToDelete = entriesToDelete;
327
- }
328
- },
329
- /**
330
- * We delete the release actions related to deleted entries
331
- * We make this only after deleteMany is succesfully executed to avoid errors
491
+ * deleteMany is still used outside documents service, for example when deleting a locale
332
492
  */
333
493
  async afterDeleteMany(event) {
334
494
  try {
335
- const { model, state } = event;
336
- const entriesToDelete = state.entriesToDelete;
337
- if (entriesToDelete) {
338
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
339
- where: {
340
- actions: {
341
- target_type: model.uid,
342
- target_id: {
343
- $in: entriesToDelete.map(
344
- (entry) => entry.id
345
- )
346
- }
347
- }
348
- }
349
- });
350
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
351
- where: {
352
- target_type: model.uid,
353
- target_id: {
354
- $in: entriesToDelete.map((entry) => entry.id)
355
- }
356
- }
495
+ const model = strapi2.getModel(event.model.uid);
496
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
497
+ const { where } = event.params;
498
+ deleteReleasesActionsAndUpdateReleaseStatus({
499
+ contentType: model.uid,
500
+ locale: where?.locale ?? null,
501
+ ...where?.documentId && { entryDocumentId: where.documentId }
357
502
  });
358
- for (const release2 of releases) {
359
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
360
- }
361
503
  }
362
504
  } catch (error) {
363
505
  strapi2.log.error("Error while deleting release actions after entry deleteMany", {
364
506
  error
365
507
  });
366
508
  }
367
- },
368
- async afterUpdate(event) {
369
- try {
370
- const { model, result } = event;
371
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
372
- const isEntryValid = await getEntryValidStatus(
373
- model.uid,
374
- result,
375
- {
376
- strapi: strapi2
377
- }
378
- );
379
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
380
- where: {
381
- target_type: model.uid,
382
- target_id: result.id
383
- },
384
- data: {
385
- isEntryValid
386
- }
387
- });
388
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
389
- where: {
390
- actions: {
391
- target_type: model.uid,
392
- target_id: result.id
393
- }
394
- }
395
- });
396
- for (const release2 of releases) {
397
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
398
- }
399
- }
400
- } catch (error) {
401
- strapi2.log.error("Error while updating release actions after entry update", { error });
402
- }
403
509
  }
404
510
  });
405
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
406
- getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
407
- strapi2.log.error(
408
- "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
409
- );
410
- throw err;
411
- });
412
- Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
413
- strapi2.webhookStore.addAllowedEvent(key, value);
414
- });
415
- }
511
+ strapi2.documents.use(deleteActionsOnDelete);
512
+ strapi2.documents.use(updateActionsOnUpdate);
513
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
514
+ strapi2.log.error(
515
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
516
+ );
517
+ throw err;
518
+ });
519
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
520
+ strapi2.get("webhookStore").addAllowedEvent(key, value);
521
+ });
416
522
  }
417
523
  };
418
524
  const destroy = async ({ strapi: strapi2 }) => {
419
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
420
- const scheduledJobs = getService("scheduling", {
421
- strapi: strapi2
422
- }).getAll();
423
- for (const [, job] of scheduledJobs) {
424
- job.cancel();
425
- }
525
+ const scheduledJobs = getService("scheduling", {
526
+ strapi: strapi2
527
+ }).getAll();
528
+ for (const [, job] of scheduledJobs) {
529
+ job.cancel();
426
530
  }
427
531
  };
428
532
  const schema$1 = {
@@ -497,15 +601,13 @@ const schema = {
497
601
  enum: ["publish", "unpublish"],
498
602
  required: true
499
603
  },
500
- entry: {
501
- type: "relation",
502
- relation: "morphToOne",
503
- configurable: false
504
- },
505
604
  contentType: {
506
605
  type: "string",
507
606
  required: true
508
607
  },
608
+ entryDocumentId: {
609
+ type: "string"
610
+ },
509
611
  locale: {
510
612
  type: "string"
511
613
  },
@@ -527,18 +629,6 @@ const contentTypes = {
527
629
  release: release$1,
528
630
  "release-action": releaseAction$1
529
631
  };
530
- const getGroupName = (queryValue) => {
531
- switch (queryValue) {
532
- case "contentType":
533
- return "contentType.displayName";
534
- case "action":
535
- return "type";
536
- case "locale":
537
- return ___default.default.getOr("No locale", "locale.name");
538
- default:
539
- return "contentType.displayName";
540
- }
541
- };
542
632
  const createReleaseService = ({ strapi: strapi2 }) => {
543
633
  const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
544
634
  strapi2.eventHub.emit(event, {
@@ -547,6 +637,33 @@ const createReleaseService = ({ strapi: strapi2 }) => {
547
637
  release: release2
548
638
  });
549
639
  };
640
+ const getFormattedActions = async (releaseId) => {
641
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
642
+ where: {
643
+ release: {
644
+ id: releaseId
645
+ }
646
+ }
647
+ });
648
+ if (actions.length === 0) {
649
+ throw new utils.errors.ValidationError("No entries to publish");
650
+ }
651
+ const formattedActions = {};
652
+ for (const action of actions) {
653
+ const contentTypeUid = action.contentType;
654
+ if (!formattedActions[contentTypeUid]) {
655
+ formattedActions[contentTypeUid] = {
656
+ publish: [],
657
+ unpublish: []
658
+ };
659
+ }
660
+ formattedActions[contentTypeUid][action.type].push({
661
+ documentId: action.entryDocumentId,
662
+ locale: action.locale
663
+ });
664
+ }
665
+ return formattedActions;
666
+ };
550
667
  return {
551
668
  async create(releaseData, { user }) {
552
669
  const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
@@ -560,13 +677,13 @@ const createReleaseService = ({ strapi: strapi2 }) => {
560
677
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
561
678
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
562
679
  ]);
563
- const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
680
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).create({
564
681
  data: {
565
682
  ...releaseWithCreatorFields,
566
683
  status: "empty"
567
684
  }
568
685
  });
569
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
686
+ if (releaseWithCreatorFields.scheduledAt) {
570
687
  const schedulingService = getService("scheduling", { strapi: strapi2 });
571
688
  await schedulingService.set(release2.id, release2.scheduledAt);
572
689
  }
@@ -574,94 +691,28 @@ const createReleaseService = ({ strapi: strapi2 }) => {
574
691
  return release2;
575
692
  },
576
693
  async findOne(id, query = {}) {
577
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
578
- ...query
694
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query);
695
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
696
+ ...dbQuery,
697
+ where: { id }
579
698
  });
580
699
  return release2;
581
700
  },
582
701
  findPage(query) {
583
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
584
- ...query,
702
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
703
+ return strapi2.db.query(RELEASE_MODEL_UID).findPage({
704
+ ...dbQuery,
585
705
  populate: {
586
706
  actions: {
587
- // @ts-expect-error Ignore missing properties
588
707
  count: true
589
708
  }
590
709
  }
591
710
  });
592
711
  },
593
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
594
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
595
- where: {
596
- actions: {
597
- target_type: contentTypeUid,
598
- target_id: entryId
599
- },
600
- releasedAt: {
601
- $null: true
602
- }
603
- },
604
- populate: {
605
- // Filter the action to get only the content type entry
606
- actions: {
607
- where: {
608
- target_type: contentTypeUid,
609
- target_id: entryId
610
- }
611
- }
612
- }
613
- });
614
- return releases.map((release2) => {
615
- if (release2.actions?.length) {
616
- const [actionForEntry] = release2.actions;
617
- delete release2.actions;
618
- return {
619
- ...release2,
620
- action: actionForEntry
621
- };
622
- }
623
- return release2;
624
- });
625
- },
626
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
627
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
628
- where: {
629
- releasedAt: {
630
- $null: true
631
- },
632
- actions: {
633
- target_type: contentTypeUid,
634
- target_id: entryId
635
- }
636
- }
637
- });
638
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
639
- where: {
640
- $or: [
641
- {
642
- id: {
643
- $notIn: releasesRelated.map((release2) => release2.id)
644
- }
645
- },
646
- {
647
- actions: null
648
- }
649
- ],
650
- releasedAt: {
651
- $null: true
652
- }
653
- }
654
- });
655
- return releases.map((release2) => {
656
- if (release2.actions?.length) {
657
- const [actionForEntry] = release2.actions;
658
- delete release2.actions;
659
- return {
660
- ...release2,
661
- action: actionForEntry
662
- };
663
- }
664
- return release2;
712
+ findMany(query) {
713
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
714
+ return strapi2.db.query(RELEASE_MODEL_UID).findMany({
715
+ ...dbQuery
665
716
  });
666
717
  },
667
718
  async update(id, releaseData, { user }) {
@@ -676,157 +727,27 @@ const createReleaseService = ({ strapi: strapi2 }) => {
676
727
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
677
728
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
678
729
  ]);
679
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
730
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id } });
680
731
  if (!release2) {
681
732
  throw new utils.errors.NotFoundError(`No release found for id ${id}`);
682
733
  }
683
734
  if (release2.releasedAt) {
684
735
  throw new utils.errors.ValidationError("Release already published");
685
736
  }
686
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
687
- /*
688
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
689
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
690
- */
691
- // @ts-expect-error see above
737
+ const updatedRelease = await strapi2.db.query(RELEASE_MODEL_UID).update({
738
+ where: { id },
692
739
  data: releaseWithCreatorFields
693
740
  });
694
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
695
- const schedulingService = getService("scheduling", { strapi: strapi2 });
696
- if (releaseData.scheduledAt) {
697
- await schedulingService.set(id, releaseData.scheduledAt);
698
- } else if (release2.scheduledAt) {
699
- schedulingService.cancel(id);
700
- }
741
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
742
+ if (releaseData.scheduledAt) {
743
+ await schedulingService.set(id, releaseData.scheduledAt);
744
+ } else if (release2.scheduledAt) {
745
+ schedulingService.cancel(id);
701
746
  }
702
747
  this.updateReleaseStatus(id);
703
748
  strapi2.telemetry.send("didUpdateContentRelease");
704
749
  return updatedRelease;
705
750
  },
706
- async createAction(releaseId, action) {
707
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
708
- strapi: strapi2
709
- });
710
- await Promise.all([
711
- validateEntryContentType(action.entry.contentType),
712
- validateUniqueEntry(releaseId, action)
713
- ]);
714
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
715
- if (!release2) {
716
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
717
- }
718
- if (release2.releasedAt) {
719
- throw new utils.errors.ValidationError("Release already published");
720
- }
721
- const { entry, type } = action;
722
- const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
723
- const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
724
- const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
725
- data: {
726
- type,
727
- contentType: entry.contentType,
728
- locale: entry.locale,
729
- isEntryValid,
730
- entry: {
731
- id: entry.id,
732
- __type: entry.contentType,
733
- __pivot: { field: "entry" }
734
- },
735
- release: releaseId
736
- },
737
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
738
- });
739
- this.updateReleaseStatus(releaseId);
740
- return releaseAction2;
741
- },
742
- async findActions(releaseId, query) {
743
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
744
- fields: ["id"]
745
- });
746
- if (!release2) {
747
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
748
- }
749
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
750
- ...query,
751
- populate: {
752
- entry: {
753
- populate: "*"
754
- }
755
- },
756
- filters: {
757
- release: releaseId
758
- }
759
- });
760
- },
761
- async countActions(query) {
762
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
763
- },
764
- async groupActions(actions, groupBy) {
765
- const contentTypeUids = actions.reduce((acc, action) => {
766
- if (!acc.includes(action.contentType)) {
767
- acc.push(action.contentType);
768
- }
769
- return acc;
770
- }, []);
771
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
772
- contentTypeUids
773
- );
774
- const allLocalesDictionary = await this.getLocalesDataForActions();
775
- const formattedData = actions.map((action) => {
776
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
777
- return {
778
- ...action,
779
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
780
- contentType: {
781
- displayName,
782
- mainFieldValue: action.entry[mainField],
783
- uid: action.contentType
784
- }
785
- };
786
- });
787
- const groupName = getGroupName(groupBy);
788
- return ___default.default.groupBy(groupName)(formattedData);
789
- },
790
- async getLocalesDataForActions() {
791
- if (!strapi2.plugin("i18n")) {
792
- return {};
793
- }
794
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
795
- return allLocales.reduce((acc, locale) => {
796
- acc[locale.code] = { name: locale.name, code: locale.code };
797
- return acc;
798
- }, {});
799
- },
800
- async getContentTypesDataForActions(contentTypesUids) {
801
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
802
- const contentTypesData = {};
803
- for (const contentTypeUid of contentTypesUids) {
804
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
805
- uid: contentTypeUid
806
- });
807
- contentTypesData[contentTypeUid] = {
808
- mainField: contentTypeConfig.settings.mainField,
809
- displayName: strapi2.getModel(contentTypeUid).info.displayName
810
- };
811
- }
812
- return contentTypesData;
813
- },
814
- getContentTypeModelsFromActions(actions) {
815
- const contentTypeUids = actions.reduce((acc, action) => {
816
- if (!acc.includes(action.contentType)) {
817
- acc.push(action.contentType);
818
- }
819
- return acc;
820
- }, []);
821
- const contentTypeModelsMap = contentTypeUids.reduce(
822
- (acc, contentTypeUid) => {
823
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
824
- return acc;
825
- },
826
- {}
827
- );
828
- return contentTypeModelsMap;
829
- },
830
751
  async getAllComponents() {
831
752
  const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
832
753
  const components = await contentManagerComponentsService.findAllComponents();
@@ -840,10 +761,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
840
761
  return componentsMap;
841
762
  },
842
763
  async delete(releaseId) {
843
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
764
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
765
+ where: { id: releaseId },
844
766
  populate: {
845
767
  actions: {
846
- fields: ["id"]
768
+ select: ["id"]
847
769
  }
848
770
  }
849
771
  });
@@ -861,9 +783,13 @@ const createReleaseService = ({ strapi: strapi2 }) => {
861
783
  }
862
784
  }
863
785
  });
864
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
786
+ await strapi2.db.query(RELEASE_MODEL_UID).delete({
787
+ where: {
788
+ id: releaseId
789
+ }
790
+ });
865
791
  });
866
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
792
+ if (release2.scheduledAt) {
867
793
  const schedulingService = getService("scheduling", { strapi: strapi2 });
868
794
  await schedulingService.cancel(release2.id);
869
795
  }
@@ -871,148 +797,294 @@ const createReleaseService = ({ strapi: strapi2 }) => {
871
797
  return release2;
872
798
  },
873
799
  async publish(releaseId) {
874
- try {
875
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
876
- RELEASE_MODEL_UID,
877
- releaseId,
878
- {
879
- populate: {
880
- actions: {
881
- populate: {
882
- entry: {
883
- fields: ["id"]
884
- }
885
- }
886
- }
887
- }
888
- }
889
- );
890
- if (!releaseWithPopulatedActionEntries) {
800
+ const {
801
+ release: release2,
802
+ error
803
+ } = await strapi2.db.transaction(async ({ trx }) => {
804
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
805
+ if (!lockedRelease) {
891
806
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
892
807
  }
893
- if (releaseWithPopulatedActionEntries.releasedAt) {
808
+ if (lockedRelease.releasedAt) {
894
809
  throw new utils.errors.ValidationError("Release already published");
895
810
  }
896
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
897
- throw new utils.errors.ValidationError("No entries to publish");
811
+ if (lockedRelease.status === "failed") {
812
+ throw new utils.errors.ValidationError("Release failed to publish");
898
813
  }
899
- const collectionTypeActions = {};
900
- const singleTypeActions = [];
901
- for (const action of releaseWithPopulatedActionEntries.actions) {
902
- const contentTypeUid = action.contentType;
903
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
904
- if (!collectionTypeActions[contentTypeUid]) {
905
- collectionTypeActions[contentTypeUid] = {
906
- entriestoPublishIds: [],
907
- entriesToUnpublishIds: []
908
- };
909
- }
910
- if (action.type === "publish") {
911
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
912
- } else {
913
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
914
- }
915
- } else {
916
- singleTypeActions.push({
917
- uid: contentTypeUid,
918
- action: action.type,
919
- id: action.entry.id
920
- });
921
- }
922
- }
923
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
924
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
925
- await strapi2.db.transaction(async () => {
926
- for (const { uid, action, id } of singleTypeActions) {
927
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
928
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
929
- try {
930
- if (action === "publish") {
931
- await entityManagerService.publish(entry, uid);
932
- } else {
933
- await entityManagerService.unpublish(entry, uid);
934
- }
935
- } catch (error) {
936
- if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft")) {
937
- } else {
938
- throw error;
939
- }
940
- }
941
- }
942
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
943
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
944
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
945
- const entriesToPublish = await strapi2.entityService.findMany(
946
- contentTypeUid,
947
- {
948
- filters: {
949
- id: {
950
- $in: entriestoPublishIds
951
- }
952
- },
953
- populate
954
- }
955
- );
956
- const entriesToUnpublish = await strapi2.entityService.findMany(
957
- contentTypeUid,
958
- {
959
- filters: {
960
- id: {
961
- $in: entriesToUnpublishIds
962
- }
963
- },
964
- populate
965
- }
966
- );
967
- if (entriesToPublish.length > 0) {
968
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
969
- }
970
- if (entriesToUnpublish.length > 0) {
971
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
972
- }
973
- }
974
- });
975
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
976
- data: {
977
- /*
978
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
979
- */
980
- // @ts-expect-error see above
981
- releasedAt: /* @__PURE__ */ new Date()
982
- },
983
- populate: {
984
- actions: {
985
- // @ts-expect-error is not expecting count but it is working
986
- count: true
814
+ try {
815
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
816
+ const formattedActions = await getFormattedActions(releaseId);
817
+ await strapi2.db.transaction(
818
+ async () => Promise.all(
819
+ Object.keys(formattedActions).map(async (contentTypeUid) => {
820
+ const contentType = contentTypeUid;
821
+ const { publish, unpublish } = formattedActions[contentType];
822
+ return Promise.all([
823
+ ...publish.map((params) => strapi2.documents(contentType).publish(params)),
824
+ ...unpublish.map((params) => strapi2.documents(contentType).unpublish(params))
825
+ ]);
826
+ })
827
+ )
828
+ );
829
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
830
+ where: {
831
+ id: releaseId
832
+ },
833
+ data: {
834
+ status: "done",
835
+ releasedAt: /* @__PURE__ */ new Date()
987
836
  }
988
- }
989
- });
990
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
837
+ });
991
838
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
992
839
  isPublished: true,
993
- release: release2
840
+ release: release22
994
841
  });
995
- }
996
- strapi2.telemetry.send("didPublishContentRelease");
997
- return release2;
998
- } catch (error) {
999
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
842
+ strapi2.telemetry.send("didPublishContentRelease");
843
+ return { release: release22, error: null };
844
+ } catch (error2) {
1000
845
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
1001
846
  isPublished: false,
1002
- error
847
+ error: error2
848
+ });
849
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
850
+ status: "failed"
851
+ }).transacting(trx).execute();
852
+ return {
853
+ release: null,
854
+ error: error2
855
+ };
856
+ }
857
+ });
858
+ if (error instanceof Error) {
859
+ throw error;
860
+ }
861
+ return release2;
862
+ },
863
+ async updateReleaseStatus(releaseId) {
864
+ const releaseActionService = getService("release-action", { strapi: strapi2 });
865
+ const [totalActions, invalidActions] = await Promise.all([
866
+ releaseActionService.countActions({
867
+ filters: {
868
+ release: releaseId
869
+ }
870
+ }),
871
+ releaseActionService.countActions({
872
+ filters: {
873
+ release: releaseId,
874
+ isEntryValid: false
875
+ }
876
+ })
877
+ ]);
878
+ if (totalActions > 0) {
879
+ if (invalidActions > 0) {
880
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
881
+ where: {
882
+ id: releaseId
883
+ },
884
+ data: {
885
+ status: "blocked"
886
+ }
1003
887
  });
1004
888
  }
1005
- strapi2.db.query(RELEASE_MODEL_UID).update({
1006
- where: { id: releaseId },
889
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
890
+ where: {
891
+ id: releaseId
892
+ },
1007
893
  data: {
1008
- status: "failed"
894
+ status: "ready"
1009
895
  }
1010
896
  });
1011
- throw error;
1012
897
  }
898
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
899
+ where: {
900
+ id: releaseId
901
+ },
902
+ data: {
903
+ status: "empty"
904
+ }
905
+ });
906
+ }
907
+ };
908
+ };
909
+ const getGroupName = (queryValue) => {
910
+ switch (queryValue) {
911
+ case "contentType":
912
+ return "contentType.displayName";
913
+ case "type":
914
+ return "type";
915
+ case "locale":
916
+ return ___default.default.getOr("No locale", "locale.name");
917
+ default:
918
+ return "contentType.displayName";
919
+ }
920
+ };
921
+ const createReleaseActionService = ({ strapi: strapi2 }) => {
922
+ const getLocalesDataForActions = async () => {
923
+ if (!strapi2.plugin("i18n")) {
924
+ return {};
925
+ }
926
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
927
+ return allLocales.reduce((acc, locale) => {
928
+ acc[locale.code] = { name: locale.name, code: locale.code };
929
+ return acc;
930
+ }, {});
931
+ };
932
+ const getContentTypesDataForActions = async (contentTypesUids) => {
933
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
934
+ const contentTypesData = {};
935
+ for (const contentTypeUid of contentTypesUids) {
936
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
937
+ uid: contentTypeUid
938
+ });
939
+ contentTypesData[contentTypeUid] = {
940
+ mainField: contentTypeConfig.settings.mainField,
941
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
942
+ };
943
+ }
944
+ return contentTypesData;
945
+ };
946
+ return {
947
+ async create(releaseId, action, { disableUpdateReleaseStatus = false } = {}) {
948
+ const { validateEntryData, validateUniqueEntry } = getService("release-validation", {
949
+ strapi: strapi2
950
+ });
951
+ await Promise.all([
952
+ validateEntryData(action.contentType, action.entryDocumentId),
953
+ validateUniqueEntry(releaseId, action)
954
+ ]);
955
+ const model = strapi2.contentType(action.contentType);
956
+ if (model.kind === "singleType") {
957
+ const document = await strapi2.db.query(model.uid).findOne({ select: ["documentId"] });
958
+ if (!document) {
959
+ throw new utils.errors.NotFoundError(`No entry found for contentType ${action.contentType}`);
960
+ }
961
+ action.entryDocumentId = document.documentId;
962
+ }
963
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId } });
964
+ if (!release2) {
965
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
966
+ }
967
+ if (release2.releasedAt) {
968
+ throw new utils.errors.ValidationError("Release already published");
969
+ }
970
+ const actionStatus = action.type === "publish" ? await getDraftEntryValidStatus(
971
+ {
972
+ contentType: action.contentType,
973
+ documentId: action.entryDocumentId,
974
+ locale: action.locale
975
+ },
976
+ {
977
+ strapi: strapi2
978
+ }
979
+ ) : true;
980
+ const releaseAction2 = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).create({
981
+ data: {
982
+ ...action,
983
+ release: release2.id,
984
+ isEntryValid: actionStatus
985
+ },
986
+ populate: { release: { select: ["id"] } }
987
+ });
988
+ if (!disableUpdateReleaseStatus) {
989
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
990
+ }
991
+ return releaseAction2;
992
+ },
993
+ async findPage(releaseId, query) {
994
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
995
+ where: { id: releaseId },
996
+ select: ["id"]
997
+ });
998
+ if (!release2) {
999
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
1000
+ }
1001
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
1002
+ const { results: actions, pagination } = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findPage({
1003
+ ...dbQuery,
1004
+ where: {
1005
+ release: releaseId
1006
+ }
1007
+ });
1008
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
1009
+ const actionsWithEntry = await utils.async.map(actions, async (action) => {
1010
+ const populate = await populateBuilderService(action.contentType).populateDeep(Infinity).build();
1011
+ const entry = await getEntry(
1012
+ {
1013
+ contentType: action.contentType,
1014
+ documentId: action.entryDocumentId,
1015
+ locale: action.locale,
1016
+ populate,
1017
+ status: action.type === "publish" ? "draft" : "published"
1018
+ },
1019
+ { strapi: strapi2 }
1020
+ );
1021
+ return {
1022
+ ...action,
1023
+ entry,
1024
+ status: entry ? await getEntryStatus(action.contentType, entry) : null
1025
+ };
1026
+ });
1027
+ return {
1028
+ results: actionsWithEntry,
1029
+ pagination
1030
+ };
1031
+ },
1032
+ async groupActions(actions, groupBy) {
1033
+ const contentTypeUids = actions.reduce((acc, action) => {
1034
+ if (!acc.includes(action.contentType)) {
1035
+ acc.push(action.contentType);
1036
+ }
1037
+ return acc;
1038
+ }, []);
1039
+ const allReleaseContentTypesDictionary = await getContentTypesDataForActions(contentTypeUids);
1040
+ const allLocalesDictionary = await getLocalesDataForActions();
1041
+ const formattedData = actions.map((action) => {
1042
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
1043
+ return {
1044
+ ...action,
1045
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
1046
+ contentType: {
1047
+ displayName,
1048
+ mainFieldValue: action.entry[mainField],
1049
+ uid: action.contentType
1050
+ }
1051
+ };
1052
+ });
1053
+ const groupName = getGroupName(groupBy);
1054
+ return ___default.default.groupBy(groupName)(formattedData);
1055
+ },
1056
+ async getContentTypeModelsFromActions(actions) {
1057
+ const contentTypeUids = actions.reduce((acc, action) => {
1058
+ if (!acc.includes(action.contentType)) {
1059
+ acc.push(action.contentType);
1060
+ }
1061
+ return acc;
1062
+ }, []);
1063
+ const workflowsService = strapi2.plugin("review-workflows").service("workflows");
1064
+ const contentTypeModelsMap = await utils.async.reduce(contentTypeUids)(
1065
+ async (accPromise, contentTypeUid) => {
1066
+ const acc = await accPromise;
1067
+ const contentTypeModel = strapi2.getModel(contentTypeUid);
1068
+ const workflow = await workflowsService.getAssignedWorkflow(contentTypeUid, {
1069
+ populate: "stageRequiredToPublish"
1070
+ });
1071
+ acc[contentTypeUid] = {
1072
+ ...contentTypeModel,
1073
+ hasReviewWorkflow: !!workflow,
1074
+ stageRequiredToPublish: workflow?.stageRequiredToPublish
1075
+ };
1076
+ return acc;
1077
+ },
1078
+ {}
1079
+ );
1080
+ return contentTypeModelsMap;
1013
1081
  },
1014
- async updateAction(actionId, releaseId, update) {
1015
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1082
+ async countActions(query) {
1083
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
1084
+ return strapi2.db.query(RELEASE_ACTION_MODEL_UID).count(dbQuery);
1085
+ },
1086
+ async update(actionId, releaseId, update) {
1087
+ const action = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findOne({
1016
1088
  where: {
1017
1089
  id: actionId,
1018
1090
  release: {
@@ -1021,17 +1093,42 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1021
1093
  $null: true
1022
1094
  }
1023
1095
  }
1024
- },
1025
- data: update
1096
+ }
1026
1097
  });
1027
- if (!updatedAction) {
1098
+ if (!action) {
1028
1099
  throw new utils.errors.NotFoundError(
1029
1100
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1030
1101
  );
1031
1102
  }
1103
+ const actionStatus = update.type === "publish" ? await getDraftEntryValidStatus(
1104
+ {
1105
+ contentType: action.contentType,
1106
+ documentId: action.entryDocumentId,
1107
+ locale: action.locale
1108
+ },
1109
+ {
1110
+ strapi: strapi2
1111
+ }
1112
+ ) : true;
1113
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1114
+ where: {
1115
+ id: actionId,
1116
+ release: {
1117
+ id: releaseId,
1118
+ releasedAt: {
1119
+ $null: true
1120
+ }
1121
+ }
1122
+ },
1123
+ data: {
1124
+ ...update,
1125
+ isEntryValid: actionStatus
1126
+ }
1127
+ });
1128
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1032
1129
  return updatedAction;
1033
1130
  },
1034
- async deleteAction(actionId, releaseId) {
1131
+ async delete(actionId, releaseId) {
1035
1132
  const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
1036
1133
  where: {
1037
1134
  id: actionId,
@@ -1048,87 +1145,104 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1048
1145
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1049
1146
  );
1050
1147
  }
1051
- this.updateReleaseStatus(releaseId);
1148
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1052
1149
  return deletedAction;
1053
1150
  },
1054
- async updateReleaseStatus(releaseId) {
1055
- const [totalActions, invalidActions] = await Promise.all([
1056
- this.countActions({
1057
- filters: {
1058
- release: releaseId
1059
- }
1060
- }),
1061
- this.countActions({
1062
- filters: {
1063
- release: releaseId,
1064
- isEntryValid: false
1065
- }
1066
- })
1067
- ]);
1068
- if (totalActions > 0) {
1069
- if (invalidActions > 0) {
1070
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1071
- where: {
1072
- id: releaseId
1073
- },
1074
- data: {
1075
- status: "blocked"
1151
+ async validateActionsByContentTypes(contentTypeUids) {
1152
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
1153
+ where: {
1154
+ contentType: {
1155
+ $in: contentTypeUids
1156
+ },
1157
+ // We only want to validate actions that are going to be published
1158
+ type: "publish",
1159
+ release: {
1160
+ releasedAt: {
1161
+ $null: true
1076
1162
  }
1077
- });
1078
- }
1079
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1163
+ }
1164
+ },
1165
+ populate: { release: true }
1166
+ });
1167
+ const releasesUpdated = [];
1168
+ await utils.async.map(actions, async (action) => {
1169
+ const isValid = await getDraftEntryValidStatus(
1170
+ {
1171
+ contentType: action.contentType,
1172
+ documentId: action.entryDocumentId,
1173
+ locale: action.locale
1174
+ },
1175
+ { strapi: strapi2 }
1176
+ );
1177
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1080
1178
  where: {
1081
- id: releaseId
1179
+ id: action.id
1082
1180
  },
1083
1181
  data: {
1084
- status: "ready"
1182
+ isEntryValid: isValid
1085
1183
  }
1086
1184
  });
1087
- }
1088
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1089
- where: {
1090
- id: releaseId
1091
- },
1092
- data: {
1093
- status: "empty"
1185
+ if (!releasesUpdated.includes(action.release.id)) {
1186
+ releasesUpdated.push(action.release.id);
1094
1187
  }
1188
+ return {
1189
+ id: action.id,
1190
+ isEntryValid: isValid
1191
+ };
1095
1192
  });
1193
+ if (releasesUpdated.length > 0) {
1194
+ await utils.async.map(releasesUpdated, async (releaseId) => {
1195
+ await getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1196
+ });
1197
+ }
1096
1198
  }
1097
1199
  };
1098
1200
  };
1201
+ class AlreadyOnReleaseError extends utils.errors.ApplicationError {
1202
+ constructor(message) {
1203
+ super(message);
1204
+ this.name = "AlreadyOnReleaseError";
1205
+ }
1206
+ }
1099
1207
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1100
1208
  async validateUniqueEntry(releaseId, releaseActionArgs) {
1101
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
1102
- populate: { actions: { populate: { entry: { fields: ["id"] } } } }
1209
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
1210
+ where: {
1211
+ id: releaseId
1212
+ },
1213
+ populate: {
1214
+ actions: true
1215
+ }
1103
1216
  });
1104
1217
  if (!release2) {
1105
1218
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
1106
1219
  }
1107
1220
  const isEntryInRelease = release2.actions.some(
1108
- (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
1221
+ (action) => action.entryDocumentId === releaseActionArgs.entryDocumentId && action.contentType === releaseActionArgs.contentType && (releaseActionArgs.locale ? action.locale === releaseActionArgs.locale : true)
1109
1222
  );
1110
1223
  if (isEntryInRelease) {
1111
- throw new utils.errors.ValidationError(
1112
- `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
1224
+ throw new AlreadyOnReleaseError(
1225
+ `Entry with documentId ${releaseActionArgs.entryDocumentId}${releaseActionArgs.locale ? `( ${releaseActionArgs.locale})` : ""} and contentType ${releaseActionArgs.contentType} already exists in release with id ${releaseId}`
1113
1226
  );
1114
1227
  }
1115
1228
  },
1116
- validateEntryContentType(contentTypeUid) {
1229
+ validateEntryData(contentTypeUid, entryDocumentId) {
1117
1230
  const contentType = strapi2.contentType(contentTypeUid);
1118
1231
  if (!contentType) {
1119
1232
  throw new utils.errors.NotFoundError(`No content type found for uid ${contentTypeUid}`);
1120
1233
  }
1121
- if (!contentType.options?.draftAndPublish) {
1234
+ if (!utils.contentTypes.hasDraftAndPublish(contentType)) {
1122
1235
  throw new utils.errors.ValidationError(
1123
1236
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
1124
1237
  );
1125
1238
  }
1239
+ if (contentType.kind === "collectionType" && !entryDocumentId) {
1240
+ throw new utils.errors.ValidationError("Document id is required for collection type");
1241
+ }
1126
1242
  },
1127
1243
  async validatePendingReleasesLimit() {
1128
- const maximumPendingReleases = (
1129
- // @ts-expect-error - options is not typed into features
1130
- EE__default.default.features.get("cms-content-releases")?.options?.maximumReleases || 3
1131
- );
1244
+ const featureCfg = strapi2.ee.features.get("cms-content-releases");
1245
+ const maximumPendingReleases = typeof featureCfg === "object" && featureCfg?.options?.maximumReleases || 3;
1132
1246
  const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
1133
1247
  filters: {
1134
1248
  releasedAt: {
@@ -1141,8 +1255,8 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1141
1255
  }
1142
1256
  },
1143
1257
  async validateUniqueNameForPendingRelease(name, id) {
1144
- const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
1145
- filters: {
1258
+ const pendingReleases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1259
+ where: {
1146
1260
  releasedAt: {
1147
1261
  $null: true
1148
1262
  },
@@ -1171,7 +1285,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1171
1285
  }
1172
1286
  const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
1173
1287
  try {
1174
- await getService("release").publish(releaseId);
1288
+ await getService("release", { strapi: strapi2 }).publish(releaseId);
1175
1289
  } catch (error) {
1176
1290
  }
1177
1291
  this.cancel(releaseId);
@@ -1213,85 +1327,172 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1213
1327
  }
1214
1328
  };
1215
1329
  };
1330
+ const DEFAULT_SETTINGS = {
1331
+ defaultTimezone: null
1332
+ };
1333
+ const createSettingsService = ({ strapi: strapi2 }) => {
1334
+ const getStore = async () => strapi2.store({ type: "core", name: "content-releases" });
1335
+ return {
1336
+ async update({ settings: settings2 }) {
1337
+ const store = await getStore();
1338
+ store.set({ key: "settings", value: settings2 });
1339
+ return settings2;
1340
+ },
1341
+ async find() {
1342
+ const store = await getStore();
1343
+ const settings2 = await store.get({ key: "settings" });
1344
+ return {
1345
+ ...DEFAULT_SETTINGS,
1346
+ ...settings2 || {}
1347
+ };
1348
+ }
1349
+ };
1350
+ };
1216
1351
  const services = {
1217
1352
  release: createReleaseService,
1353
+ "release-action": createReleaseActionService,
1218
1354
  "release-validation": createReleaseValidationService,
1219
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1355
+ scheduling: createSchedulingService,
1356
+ settings: createSettingsService
1220
1357
  };
1221
- const RELEASE_SCHEMA = yup__namespace.object().shape({
1222
- name: yup__namespace.string().trim().required(),
1223
- scheduledAt: yup__namespace.string().nullable(),
1224
- isScheduled: yup__namespace.boolean().optional(),
1225
- time: yup__namespace.string().when("isScheduled", {
1226
- is: true,
1227
- then: yup__namespace.string().trim().required(),
1228
- otherwise: yup__namespace.string().nullable()
1229
- }),
1230
- timezone: yup__namespace.string().when("isScheduled", {
1231
- is: true,
1232
- then: yup__namespace.string().required().nullable(),
1233
- otherwise: yup__namespace.string().nullable()
1234
- }),
1235
- date: yup__namespace.string().when("isScheduled", {
1236
- is: true,
1237
- then: yup__namespace.string().required().nullable(),
1238
- otherwise: yup__namespace.string().nullable()
1358
+ const RELEASE_SCHEMA = utils.yup.object().shape({
1359
+ name: utils.yup.string().trim().required(),
1360
+ scheduledAt: utils.yup.string().nullable(),
1361
+ timezone: utils.yup.string().when("scheduledAt", {
1362
+ is: (value) => value !== null && value !== void 0,
1363
+ then: utils.yup.string().required(),
1364
+ otherwise: utils.yup.string().nullable()
1239
1365
  })
1240
1366
  }).required().noUnknown();
1367
+ const FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA = utils.yup.object().shape({
1368
+ contentType: utils.yup.string().required(),
1369
+ entryDocumentId: utils.yup.string().nullable(),
1370
+ hasEntryAttached: utils.yup.string().nullable(),
1371
+ locale: utils.yup.string().nullable()
1372
+ }).required().noUnknown();
1241
1373
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
1374
+ const validatefindByDocumentAttachedParams = utils.validateYupSchema(
1375
+ FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA
1376
+ );
1242
1377
  const releaseController = {
1243
- async findMany(ctx) {
1244
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1378
+ /**
1379
+ * Find releases based on documents attached or not to the release.
1380
+ * If `hasEntryAttached` is true, it will return all releases that have the entry attached.
1381
+ * If `hasEntryAttached` is false, it will return all releases that don't have the entry attached.
1382
+ */
1383
+ async findByDocumentAttached(ctx) {
1384
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1245
1385
  ability: ctx.state.userAbility,
1246
1386
  model: RELEASE_MODEL_UID
1247
1387
  });
1248
1388
  await permissionsManager.validateQuery(ctx.query);
1249
1389
  const releaseService = getService("release", { strapi });
1250
- const isFindManyForContentTypeEntry = Boolean(ctx.query?.contentTypeUid && ctx.query?.entryId);
1251
- if (isFindManyForContentTypeEntry) {
1252
- const query = await permissionsManager.sanitizeQuery(ctx.query);
1253
- const contentTypeUid = query.contentTypeUid;
1254
- const entryId = query.entryId;
1255
- const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
1256
- const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
1257
- ctx.body = { data };
1258
- } else {
1259
- const query = await permissionsManager.sanitizeQuery(ctx.query);
1260
- const { results, pagination } = await releaseService.findPage(query);
1261
- const data = results.map((release2) => {
1262
- const { actions, ...releaseData } = release2;
1263
- return {
1264
- ...releaseData,
1390
+ const query = await permissionsManager.sanitizeQuery(ctx.query);
1391
+ await validatefindByDocumentAttachedParams(query);
1392
+ const model = strapi.getModel(query.contentType);
1393
+ if (model.kind && model.kind === "singleType") {
1394
+ const document = await strapi.db.query(model.uid).findOne({ select: ["documentId"] });
1395
+ if (!document) {
1396
+ throw new utils.errors.NotFoundError(`No entry found for contentType ${query.contentType}`);
1397
+ }
1398
+ query.entryDocumentId = document.documentId;
1399
+ }
1400
+ const { contentType, hasEntryAttached, entryDocumentId, locale } = query;
1401
+ const isEntryAttached = typeof hasEntryAttached === "string" ? Boolean(JSON.parse(hasEntryAttached)) : false;
1402
+ if (isEntryAttached) {
1403
+ const releases = await releaseService.findMany({
1404
+ where: {
1405
+ releasedAt: null,
1265
1406
  actions: {
1266
- meta: {
1267
- count: actions.count
1407
+ contentType,
1408
+ entryDocumentId: entryDocumentId ?? null,
1409
+ locale: locale ?? null
1410
+ }
1411
+ },
1412
+ populate: {
1413
+ actions: {
1414
+ fields: ["type"],
1415
+ filters: {
1416
+ contentType,
1417
+ entryDocumentId: entryDocumentId ?? null,
1418
+ locale: locale ?? null
1268
1419
  }
1269
1420
  }
1270
- };
1421
+ }
1422
+ });
1423
+ ctx.body = { data: releases };
1424
+ } else {
1425
+ const relatedReleases = await releaseService.findMany({
1426
+ where: {
1427
+ releasedAt: null,
1428
+ actions: {
1429
+ contentType,
1430
+ entryDocumentId: entryDocumentId ?? null,
1431
+ locale: locale ?? null
1432
+ }
1433
+ }
1271
1434
  });
1272
- const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1435
+ const releases = await releaseService.findMany({
1273
1436
  where: {
1437
+ $or: [
1438
+ {
1439
+ id: {
1440
+ $notIn: relatedReleases.map((release2) => release2.id)
1441
+ }
1442
+ },
1443
+ {
1444
+ actions: null
1445
+ }
1446
+ ],
1274
1447
  releasedAt: null
1275
1448
  }
1276
1449
  });
1277
- ctx.body = { data, meta: { pagination, pendingReleasesCount } };
1450
+ ctx.body = { data: releases };
1278
1451
  }
1279
1452
  },
1453
+ async findPage(ctx) {
1454
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1455
+ ability: ctx.state.userAbility,
1456
+ model: RELEASE_MODEL_UID
1457
+ });
1458
+ await permissionsManager.validateQuery(ctx.query);
1459
+ const releaseService = getService("release", { strapi });
1460
+ const query = await permissionsManager.sanitizeQuery(ctx.query);
1461
+ const { results, pagination } = await releaseService.findPage(query);
1462
+ const data = results.map((release2) => {
1463
+ const { actions, ...releaseData } = release2;
1464
+ return {
1465
+ ...releaseData,
1466
+ actions: {
1467
+ meta: {
1468
+ count: actions.count
1469
+ }
1470
+ }
1471
+ };
1472
+ });
1473
+ const pendingReleasesCount = await strapi.db.query(RELEASE_MODEL_UID).count({
1474
+ where: {
1475
+ releasedAt: null
1476
+ }
1477
+ });
1478
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
1479
+ },
1280
1480
  async findOne(ctx) {
1281
1481
  const id = ctx.params.id;
1282
1482
  const releaseService = getService("release", { strapi });
1483
+ const releaseActionService = getService("release-action", { strapi });
1283
1484
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
1284
1485
  if (!release2) {
1285
1486
  throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
1286
1487
  }
1287
- const count = await releaseService.countActions({
1488
+ const count = await releaseActionService.countActions({
1288
1489
  filters: {
1289
1490
  release: id
1290
1491
  }
1291
1492
  });
1292
1493
  const sanitizedRelease = {
1293
1494
  ...release2,
1294
- createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1495
+ createdBy: release2.createdBy ? strapi.service("admin::user").sanitizeUser(release2.createdBy) : null
1295
1496
  };
1296
1497
  const data = {
1297
1498
  ...sanitizedRelease,
@@ -1303,19 +1504,63 @@ const releaseController = {
1303
1504
  };
1304
1505
  ctx.body = { data };
1305
1506
  },
1507
+ async mapEntriesToReleases(ctx) {
1508
+ const { contentTypeUid, documentIds, locale } = ctx.query;
1509
+ if (!contentTypeUid || !documentIds) {
1510
+ throw new utils.errors.ValidationError("Missing required query parameters");
1511
+ }
1512
+ const releaseService = getService("release", { strapi });
1513
+ const releasesWithActions = await releaseService.findMany({
1514
+ where: {
1515
+ releasedAt: null,
1516
+ actions: {
1517
+ contentType: contentTypeUid,
1518
+ entryDocumentId: {
1519
+ $in: documentIds
1520
+ },
1521
+ locale
1522
+ }
1523
+ },
1524
+ populate: {
1525
+ actions: true
1526
+ }
1527
+ });
1528
+ const mappedEntriesInReleases = releasesWithActions.reduce(
1529
+ (acc, release2) => {
1530
+ release2.actions.forEach((action) => {
1531
+ if (action.contentType !== contentTypeUid) {
1532
+ return;
1533
+ }
1534
+ if (locale && action.locale !== locale) {
1535
+ return;
1536
+ }
1537
+ if (!acc[action.entryDocumentId]) {
1538
+ acc[action.entryDocumentId] = [{ id: release2.id, name: release2.name }];
1539
+ } else {
1540
+ acc[action.entryDocumentId].push({ id: release2.id, name: release2.name });
1541
+ }
1542
+ });
1543
+ return acc;
1544
+ },
1545
+ {}
1546
+ );
1547
+ ctx.body = {
1548
+ data: mappedEntriesInReleases
1549
+ };
1550
+ },
1306
1551
  async create(ctx) {
1307
1552
  const user = ctx.state.user;
1308
1553
  const releaseArgs = ctx.request.body;
1309
1554
  await validateRelease(releaseArgs);
1310
1555
  const releaseService = getService("release", { strapi });
1311
1556
  const release2 = await releaseService.create(releaseArgs, { user });
1312
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1557
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1313
1558
  ability: ctx.state.userAbility,
1314
1559
  model: RELEASE_MODEL_UID
1315
1560
  });
1316
- ctx.body = {
1561
+ ctx.created({
1317
1562
  data: await permissionsManager.sanitizeOutput(release2)
1318
- };
1563
+ });
1319
1564
  },
1320
1565
  async update(ctx) {
1321
1566
  const user = ctx.state.user;
@@ -1324,7 +1569,7 @@ const releaseController = {
1324
1569
  await validateRelease(releaseArgs);
1325
1570
  const releaseService = getService("release", { strapi });
1326
1571
  const release2 = await releaseService.update(id, releaseArgs, { user });
1327
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1572
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1328
1573
  ability: ctx.state.userAbility,
1329
1574
  model: RELEASE_MODEL_UID
1330
1575
  });
@@ -1341,18 +1586,18 @@ const releaseController = {
1341
1586
  };
1342
1587
  },
1343
1588
  async publish(ctx) {
1344
- const user = ctx.state.user;
1345
1589
  const id = ctx.params.id;
1346
1590
  const releaseService = getService("release", { strapi });
1347
- const release2 = await releaseService.publish(id, { user });
1591
+ const releaseActionService = getService("release-action", { strapi });
1592
+ const release2 = await releaseService.publish(id);
1348
1593
  const [countPublishActions, countUnpublishActions] = await Promise.all([
1349
- releaseService.countActions({
1594
+ releaseActionService.countActions({
1350
1595
  filters: {
1351
1596
  release: id,
1352
1597
  type: "publish"
1353
1598
  }
1354
1599
  }),
1355
- releaseService.countActions({
1600
+ releaseActionService.countActions({
1356
1601
  filters: {
1357
1602
  release: id,
1358
1603
  type: "unpublish"
@@ -1370,57 +1615,106 @@ const releaseController = {
1370
1615
  }
1371
1616
  };
1372
1617
  const RELEASE_ACTION_SCHEMA = utils.yup.object().shape({
1373
- entry: utils.yup.object().shape({
1374
- id: utils.yup.strapiID().required(),
1375
- contentType: utils.yup.string().required()
1376
- }).required(),
1618
+ contentType: utils.yup.string().required(),
1619
+ entryDocumentId: utils.yup.strapiID(),
1620
+ locale: utils.yup.string(),
1377
1621
  type: utils.yup.string().oneOf(["publish", "unpublish"]).required()
1378
1622
  });
1379
1623
  const RELEASE_ACTION_UPDATE_SCHEMA = utils.yup.object().shape({
1380
1624
  type: utils.yup.string().oneOf(["publish", "unpublish"]).required()
1381
1625
  });
1626
+ const FIND_MANY_ACTIONS_PARAMS = utils.yup.object().shape({
1627
+ groupBy: utils.yup.string().oneOf(["action", "contentType", "locale"])
1628
+ });
1382
1629
  const validateReleaseAction = utils.validateYupSchema(RELEASE_ACTION_SCHEMA);
1383
1630
  const validateReleaseActionUpdateSchema = utils.validateYupSchema(RELEASE_ACTION_UPDATE_SCHEMA);
1631
+ const validateFindManyActionsParams = utils.validateYupSchema(FIND_MANY_ACTIONS_PARAMS);
1384
1632
  const releaseActionController = {
1385
1633
  async create(ctx) {
1386
1634
  const releaseId = ctx.params.releaseId;
1387
1635
  const releaseActionArgs = ctx.request.body;
1388
1636
  await validateReleaseAction(releaseActionArgs);
1389
- const releaseService = getService("release", { strapi });
1390
- const releaseAction2 = await releaseService.createAction(releaseId, releaseActionArgs);
1391
- ctx.body = {
1637
+ const releaseActionService = getService("release-action", { strapi });
1638
+ const releaseAction2 = await releaseActionService.create(releaseId, releaseActionArgs);
1639
+ ctx.created({
1392
1640
  data: releaseAction2
1393
- };
1641
+ });
1642
+ },
1643
+ async createMany(ctx) {
1644
+ const releaseId = ctx.params.releaseId;
1645
+ const releaseActionsArgs = ctx.request.body;
1646
+ await Promise.all(
1647
+ releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1648
+ );
1649
+ const releaseActionService = getService("release-action", { strapi });
1650
+ const releaseService = getService("release", { strapi });
1651
+ const releaseActions = await strapi.db.transaction(async () => {
1652
+ const releaseActions2 = await Promise.all(
1653
+ releaseActionsArgs.map(async (releaseActionArgs) => {
1654
+ try {
1655
+ const action = await releaseActionService.create(releaseId, releaseActionArgs, {
1656
+ disableUpdateReleaseStatus: true
1657
+ });
1658
+ return action;
1659
+ } catch (error) {
1660
+ if (error instanceof AlreadyOnReleaseError) {
1661
+ return null;
1662
+ }
1663
+ throw error;
1664
+ }
1665
+ })
1666
+ );
1667
+ return releaseActions2;
1668
+ });
1669
+ const newReleaseActions = releaseActions.filter((action) => action !== null);
1670
+ if (newReleaseActions.length > 0) {
1671
+ releaseService.updateReleaseStatus(releaseId);
1672
+ }
1673
+ ctx.created({
1674
+ data: newReleaseActions,
1675
+ meta: {
1676
+ entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1677
+ totalEntries: releaseActions.length
1678
+ }
1679
+ });
1394
1680
  },
1395
1681
  async findMany(ctx) {
1396
1682
  const releaseId = ctx.params.releaseId;
1397
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1683
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1398
1684
  ability: ctx.state.userAbility,
1399
1685
  model: RELEASE_ACTION_MODEL_UID
1400
1686
  });
1687
+ await validateFindManyActionsParams(ctx.query);
1688
+ if (ctx.query.groupBy) {
1689
+ if (!["action", "contentType", "locale"].includes(ctx.query.groupBy)) {
1690
+ ctx.badRequest("Invalid groupBy parameter");
1691
+ }
1692
+ }
1693
+ ctx.query.sort = ctx.query.groupBy === "action" ? "type" : ctx.query.groupBy;
1694
+ delete ctx.query.groupBy;
1401
1695
  const query = await permissionsManager.sanitizeQuery(ctx.query);
1402
- const releaseService = getService("release", { strapi });
1403
- const { results, pagination } = await releaseService.findActions(releaseId, {
1404
- sort: query.groupBy === "action" ? "type" : query.groupBy,
1696
+ const releaseActionService = getService("release-action", { strapi });
1697
+ const { results, pagination } = await releaseActionService.findPage(releaseId, {
1405
1698
  ...query
1406
1699
  });
1407
1700
  const contentTypeOutputSanitizers = results.reduce((acc, action) => {
1408
1701
  if (acc[action.contentType]) {
1409
1702
  return acc;
1410
1703
  }
1411
- const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
1704
+ const contentTypePermissionsManager = strapi.service("admin::permission").createPermissionsManager({
1412
1705
  ability: ctx.state.userAbility,
1413
1706
  model: action.contentType
1414
1707
  });
1415
1708
  acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
1416
1709
  return acc;
1417
1710
  }, {});
1418
- const sanitizedResults = await utils.mapAsync(results, async (action) => ({
1711
+ const sanitizedResults = await utils.async.map(results, async (action) => ({
1419
1712
  ...action,
1420
- entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1713
+ entry: action.entry ? await contentTypeOutputSanitizers[action.contentType](action.entry) : {}
1421
1714
  }));
1422
- const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
1423
- const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
1715
+ const groupedData = await releaseActionService.groupActions(sanitizedResults, query.sort);
1716
+ const contentTypes2 = await releaseActionService.getContentTypeModelsFromActions(results);
1717
+ const releaseService = getService("release", { strapi });
1424
1718
  const components = await releaseService.getAllComponents();
1425
1719
  ctx.body = {
1426
1720
  data: groupedData,
@@ -1436,8 +1730,8 @@ const releaseActionController = {
1436
1730
  const releaseId = ctx.params.releaseId;
1437
1731
  const releaseActionUpdateArgs = ctx.request.body;
1438
1732
  await validateReleaseActionUpdateSchema(releaseActionUpdateArgs);
1439
- const releaseService = getService("release", { strapi });
1440
- const updatedAction = await releaseService.updateAction(
1733
+ const releaseActionService = getService("release-action", { strapi });
1734
+ const updatedAction = await releaseActionService.update(
1441
1735
  actionId,
1442
1736
  releaseId,
1443
1737
  releaseActionUpdateArgs
@@ -1449,17 +1743,71 @@ const releaseActionController = {
1449
1743
  async delete(ctx) {
1450
1744
  const actionId = ctx.params.actionId;
1451
1745
  const releaseId = ctx.params.releaseId;
1452
- const releaseService = getService("release", { strapi });
1453
- const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
1746
+ const releaseActionService = getService("release-action", { strapi });
1747
+ const deletedReleaseAction = await releaseActionService.delete(actionId, releaseId);
1454
1748
  ctx.body = {
1455
1749
  data: deletedReleaseAction
1456
1750
  };
1457
1751
  }
1458
1752
  };
1459
- const controllers = { release: releaseController, "release-action": releaseActionController };
1753
+ const SETTINGS_SCHEMA = yup__namespace.object().shape({
1754
+ defaultTimezone: yup__namespace.string().nullable().default(null)
1755
+ }).required().noUnknown();
1756
+ const validateSettings = utils.validateYupSchema(SETTINGS_SCHEMA);
1757
+ const settingsController = {
1758
+ async find(ctx) {
1759
+ const settingsService = getService("settings", { strapi });
1760
+ const settings2 = await settingsService.find();
1761
+ ctx.body = { data: settings2 };
1762
+ },
1763
+ async update(ctx) {
1764
+ const settingsBody = ctx.request.body;
1765
+ const settings2 = await validateSettings(settingsBody);
1766
+ const settingsService = getService("settings", { strapi });
1767
+ const updatedSettings = await settingsService.update({ settings: settings2 });
1768
+ ctx.body = { data: updatedSettings };
1769
+ }
1770
+ };
1771
+ const controllers = {
1772
+ release: releaseController,
1773
+ "release-action": releaseActionController,
1774
+ settings: settingsController
1775
+ };
1460
1776
  const release = {
1461
1777
  type: "admin",
1462
1778
  routes: [
1779
+ {
1780
+ method: "GET",
1781
+ path: "/mapEntriesToReleases",
1782
+ handler: "release.mapEntriesToReleases",
1783
+ config: {
1784
+ policies: [
1785
+ "admin::isAuthenticatedAdmin",
1786
+ {
1787
+ name: "admin::hasPermissions",
1788
+ config: {
1789
+ actions: ["plugin::content-releases.read"]
1790
+ }
1791
+ }
1792
+ ]
1793
+ }
1794
+ },
1795
+ {
1796
+ method: "GET",
1797
+ path: "/getByDocumentAttached",
1798
+ handler: "release.findByDocumentAttached",
1799
+ config: {
1800
+ policies: [
1801
+ "admin::isAuthenticatedAdmin",
1802
+ {
1803
+ name: "admin::hasPermissions",
1804
+ config: {
1805
+ actions: ["plugin::content-releases.read"]
1806
+ }
1807
+ }
1808
+ ]
1809
+ }
1810
+ },
1463
1811
  {
1464
1812
  method: "POST",
1465
1813
  path: "/",
@@ -1479,7 +1827,7 @@ const release = {
1479
1827
  {
1480
1828
  method: "GET",
1481
1829
  path: "/",
1482
- handler: "release.findMany",
1830
+ handler: "release.findPage",
1483
1831
  config: {
1484
1832
  policies: [
1485
1833
  "admin::isAuthenticatedAdmin",
@@ -1577,6 +1925,22 @@ const releaseAction = {
1577
1925
  ]
1578
1926
  }
1579
1927
  },
1928
+ {
1929
+ method: "POST",
1930
+ path: "/:releaseId/actions/bulk",
1931
+ handler: "release-action.createMany",
1932
+ config: {
1933
+ policies: [
1934
+ "admin::isAuthenticatedAdmin",
1935
+ {
1936
+ name: "admin::hasPermissions",
1937
+ config: {
1938
+ actions: ["plugin::content-releases.create-action"]
1939
+ }
1940
+ }
1941
+ ]
1942
+ }
1943
+ },
1580
1944
  {
1581
1945
  method: "GET",
1582
1946
  path: "/:releaseId/actions",
@@ -1627,13 +1991,50 @@ const releaseAction = {
1627
1991
  }
1628
1992
  ]
1629
1993
  };
1994
+ const settings = {
1995
+ type: "admin",
1996
+ routes: [
1997
+ {
1998
+ method: "GET",
1999
+ path: "/settings",
2000
+ handler: "settings.find",
2001
+ config: {
2002
+ policies: [
2003
+ "admin::isAuthenticatedAdmin",
2004
+ {
2005
+ name: "admin::hasPermissions",
2006
+ config: {
2007
+ actions: ["plugin::content-releases.settings.read"]
2008
+ }
2009
+ }
2010
+ ]
2011
+ }
2012
+ },
2013
+ {
2014
+ method: "PUT",
2015
+ path: "/settings",
2016
+ handler: "settings.update",
2017
+ config: {
2018
+ policies: [
2019
+ "admin::isAuthenticatedAdmin",
2020
+ {
2021
+ name: "admin::hasPermissions",
2022
+ config: {
2023
+ actions: ["plugin::content-releases.settings.update"]
2024
+ }
2025
+ }
2026
+ ]
2027
+ }
2028
+ }
2029
+ ]
2030
+ };
1630
2031
  const routes = {
2032
+ settings,
1631
2033
  release,
1632
2034
  "release-action": releaseAction
1633
2035
  };
1634
- const { features } = require("@strapi/strapi/dist/utils/ee");
1635
2036
  const getPlugin = () => {
1636
- if (features.isEnabled("cms-content-releases")) {
2037
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
1637
2038
  return {
1638
2039
  register,
1639
2040
  bootstrap,
@@ -1645,6 +2046,9 @@ const getPlugin = () => {
1645
2046
  };
1646
2047
  }
1647
2048
  return {
2049
+ // Always return register, it handles its own feature check
2050
+ register,
2051
+ // Always return contentTypes to avoid losing data when the feature is disabled
1648
2052
  contentTypes
1649
2053
  };
1650
2054
  };