@strapi/content-releases 0.0.0-next.f8af92b375dc730ba47ed2117f25df893aae696c → 0.0.0-next.fc1775f7731f8999840e56e298a216b9a6c5c4ad

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-CiZCkScI.mjs +1558 -0
  3. package/dist/_chunks/App-CiZCkScI.mjs.map +1 -0
  4. package/dist/_chunks/App-SGjO5UPV.js +1578 -0
  5. package/dist/_chunks/App-SGjO5UPV.js.map +1 -0
  6. package/dist/_chunks/{PurchaseContentReleases-YhAPgpG9.js → PurchaseContentReleases--qQepXpP.js} +9 -8
  7. package/dist/_chunks/PurchaseContentReleases--qQepXpP.js.map +1 -0
  8. package/dist/_chunks/{PurchaseContentReleases-Clm0iACO.mjs → PurchaseContentReleases-D-n-w-st.mjs} +10 -9
  9. package/dist/_chunks/PurchaseContentReleases-D-n-w-st.mjs.map +1 -0
  10. package/dist/_chunks/ReleasesSettingsPage-Cto_NLUd.js +178 -0
  11. package/dist/_chunks/ReleasesSettingsPage-Cto_NLUd.js.map +1 -0
  12. package/dist/_chunks/ReleasesSettingsPage-DQT8N3A-.mjs +178 -0
  13. package/dist/_chunks/ReleasesSettingsPage-DQT8N3A-.mjs.map +1 -0
  14. package/dist/_chunks/{en-r0otWaln.js → en-BWPPsSH-.js} +31 -6
  15. package/dist/_chunks/en-BWPPsSH-.js.map +1 -0
  16. package/dist/_chunks/{en-veqvqeEr.mjs → en-D9Q4YW03.mjs} +31 -6
  17. package/dist/_chunks/en-D9Q4YW03.mjs.map +1 -0
  18. package/dist/_chunks/index-BjvFfTtA.mjs +1386 -0
  19. package/dist/_chunks/index-BjvFfTtA.mjs.map +1 -0
  20. package/dist/_chunks/index-CyU534vL.js +1404 -0
  21. package/dist/_chunks/index-CyU534vL.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 +1233 -515
  57. package/dist/server/index.js.map +1 -1
  58. package/dist/server/index.mjs +1232 -513
  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 -40
  131. package/dist/_chunks/App-OK4Xac-O.js +0 -1315
  132. package/dist/_chunks/App-OK4Xac-O.js.map +0 -1
  133. package/dist/_chunks/App-xAkiD42p.mjs +0 -1292
  134. package/dist/_chunks/App-xAkiD42p.mjs.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-r0otWaln.js.map +0 -1
  138. package/dist/_chunks/en-veqvqeEr.mjs.map +0 -1
  139. package/dist/_chunks/index-JvA2_26n.js +0 -1015
  140. package/dist/_chunks/index-JvA2_26n.js.map +0 -1
  141. package/dist/_chunks/index-exoiSU3V.mjs +0 -994
  142. package/dist/_chunks/index-exoiSU3V.mjs.map +0 -1
  143. package/strapi-server.js +0 -3
@@ -1,14 +1,13 @@
1
1
  "use strict";
2
2
  const utils = require("@strapi/utils");
3
+ const isEqual = require("lodash/isEqual");
3
4
  const lodash = require("lodash");
4
5
  const _ = require("lodash/fp");
5
- const EE = require("@strapi/strapi/dist/utils/ee");
6
6
  const nodeSchedule = require("node-schedule");
7
7
  const yup = require("yup");
8
8
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
9
9
  function _interopNamespace(e) {
10
- if (e && e.__esModule)
11
- return e;
10
+ if (e && e.__esModule) return e;
12
11
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
13
12
  if (e) {
14
13
  for (const k in e) {
@@ -24,8 +23,8 @@ function _interopNamespace(e) {
24
23
  n.default = e;
25
24
  return Object.freeze(n);
26
25
  }
26
+ const isEqual__default = /* @__PURE__ */ _interopDefault(isEqual);
27
27
  const ___default = /* @__PURE__ */ _interopDefault(_);
28
- const EE__default = /* @__PURE__ */ _interopDefault(EE);
29
28
  const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
30
29
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
31
30
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -71,11 +70,94 @@ const ACTIONS = [
71
70
  displayName: "Add an entry to a release",
72
71
  uid: "create-action",
73
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"
74
90
  }
75
91
  ];
76
92
  const ALLOWED_WEBHOOK_EVENTS = {
77
93
  RELEASES_PUBLISH: "releases.publish"
78
94
  };
95
+ const getService = (name, { strapi: strapi2 }) => {
96
+ return strapi2.plugin("content-releases").service(name);
97
+ };
98
+ const getDraftEntryValidStatus = async ({ contentType, documentId, locale }, { strapi: strapi2 }) => {
99
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
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 });
103
+ };
104
+ const isEntryValid = async (contentTypeUid, entry, { strapi: strapi2 }) => {
105
+ try {
106
+ await strapi2.entityValidator.validateEntityCreation(
107
+ strapi2.getModel(contentTypeUid),
108
+ entry,
109
+ void 0,
110
+ // @ts-expect-error - FIXME: entity here is unnecessary
111
+ entry
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
+ }
120
+ return true;
121
+ } catch {
122
+ return false;
123
+ }
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
+ };
79
161
  async function deleteActionsOnDisableDraftAndPublish({
80
162
  oldContentTypes,
81
163
  contentTypes: contentTypes2
@@ -97,90 +179,354 @@ async function deleteActionsOnDisableDraftAndPublish({
97
179
  async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
98
180
  const deletedContentTypes = lodash.difference(lodash.keys(oldContentTypes), lodash.keys(contentTypes2)) ?? [];
99
181
  if (deletedContentTypes.length) {
100
- await utils.mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
182
+ await utils.async.map(deletedContentTypes, async (deletedContentTypeUID) => {
101
183
  return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
102
184
  });
103
185
  }
104
186
  }
105
- const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
187
+ async function migrateIsValidAndStatusReleases() {
188
+ const releasesWithoutStatus = await strapi.db.query(RELEASE_MODEL_UID).findMany({
189
+ where: {
190
+ status: null,
191
+ releasedAt: null
192
+ },
193
+ populate: {
194
+ actions: {
195
+ populate: {
196
+ entry: true
197
+ }
198
+ }
199
+ }
200
+ });
201
+ utils.async.map(releasesWithoutStatus, async (release2) => {
202
+ const actions = release2.actions;
203
+ const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
204
+ for (const action of notValidatedActions) {
205
+ if (action.entry) {
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
+ }
221
+ });
222
+ }
223
+ }
224
+ return getService("release", { strapi }).updateReleaseStatus(release2.id);
225
+ });
226
+ const publishedReleases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
227
+ where: {
228
+ status: null,
229
+ releasedAt: {
230
+ $notNull: true
231
+ }
232
+ }
233
+ });
234
+ utils.async.map(publishedReleases, async (release2) => {
235
+ return strapi.db.query(RELEASE_MODEL_UID).update({
236
+ where: {
237
+ id: release2.id
238
+ },
239
+ data: {
240
+ status: "done"
241
+ }
242
+ });
243
+ });
244
+ }
245
+ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: contentTypes2 }) {
246
+ if (oldContentTypes !== void 0 && contentTypes2 !== void 0) {
247
+ const contentTypesWithDraftAndPublish = Object.keys(oldContentTypes).filter(
248
+ (uid) => oldContentTypes[uid]?.options?.draftAndPublish
249
+ );
250
+ const releasesAffected = /* @__PURE__ */ new Set();
251
+ utils.async.map(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
252
+ const oldContentType = oldContentTypes[contentTypeUID];
253
+ const contentType = contentTypes2[contentTypeUID];
254
+ if (!isEqual__default.default(oldContentType?.attributes, contentType?.attributes)) {
255
+ const actions = await strapi.db.query(RELEASE_ACTION_MODEL_UID).findMany({
256
+ where: {
257
+ contentType: contentTypeUID
258
+ },
259
+ populate: {
260
+ entry: true,
261
+ release: true
262
+ }
263
+ });
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
+ }
282
+ });
283
+ }
284
+ });
285
+ }
286
+ }).then(() => {
287
+ utils.async.map(releasesAffected, async (releaseId) => {
288
+ return getService("release", { strapi }).updateReleaseStatus(releaseId);
289
+ });
290
+ });
291
+ }
292
+ }
293
+ async function disableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
294
+ if (!oldContentTypes) {
295
+ return;
296
+ }
297
+ const i18nPlugin = strapi.plugin("i18n");
298
+ if (!i18nPlugin) {
299
+ return;
300
+ }
301
+ for (const uid in contentTypes2) {
302
+ if (!oldContentTypes[uid]) {
303
+ continue;
304
+ }
305
+ const oldContentType = oldContentTypes[uid];
306
+ const contentType = contentTypes2[uid];
307
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
308
+ if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
309
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
310
+ locale: null
311
+ }).where({ contentType: uid }).execute();
312
+ }
313
+ }
314
+ }
315
+ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
316
+ if (!oldContentTypes) {
317
+ return;
318
+ }
319
+ const i18nPlugin = strapi.plugin("i18n");
320
+ if (!i18nPlugin) {
321
+ return;
322
+ }
323
+ for (const uid in contentTypes2) {
324
+ if (!oldContentTypes[uid]) {
325
+ continue;
326
+ }
327
+ const oldContentType = oldContentTypes[uid];
328
+ const contentType = contentTypes2[uid];
329
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
330
+ const { getDefaultLocale } = i18nPlugin.service("locales");
331
+ if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
332
+ const defaultLocale = await getDefaultLocale();
333
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
334
+ locale: defaultLocale
335
+ }).where({ contentType: uid }).execute();
336
+ }
337
+ }
338
+ }
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
+ };
106
371
  const register = async ({ strapi: strapi2 }) => {
107
- if (features$2.isEnabled("cms-content-releases")) {
108
- await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
109
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
110
- strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
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);
376
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
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();
111
382
  }
112
383
  };
113
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
114
- return strapi2.plugin("content-releases").service(name);
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
+ }
115
482
  };
116
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
117
483
  const bootstrap = async ({ strapi: strapi2 }) => {
118
- if (features$1.isEnabled("cms-content-releases")) {
484
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
485
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
486
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
487
+ );
119
488
  strapi2.db.lifecycles.subscribe({
120
- afterDelete(event) {
121
- const { model, result } = event;
122
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
123
- const { id } = result;
124
- strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
125
- where: {
126
- target_type: model.uid,
127
- target_id: id
128
- }
129
- });
130
- }
131
- },
132
- /**
133
- * deleteMany hook doesn't return the deleted entries ids
134
- * so we need to fetch them before deleting the entries to save the ids on our state
135
- */
136
- async beforeDeleteMany(event) {
137
- const { model, params } = event;
138
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
139
- const { where } = params;
140
- const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
141
- event.state.entriesToDelete = entriesToDelete;
142
- }
143
- },
489
+ models: contentTypesWithDraftAndPublish,
144
490
  /**
145
- * We delete the release actions related to deleted entries
146
- * 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
147
492
  */
148
493
  async afterDeleteMany(event) {
149
- const { model, state } = event;
150
- const entriesToDelete = state.entriesToDelete;
151
- if (entriesToDelete) {
152
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
153
- where: {
154
- target_type: model.uid,
155
- target_id: {
156
- $in: entriesToDelete.map((entry) => entry.id)
157
- }
158
- }
494
+ try {
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 }
502
+ });
503
+ }
504
+ } catch (error) {
505
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
506
+ error
159
507
  });
160
508
  }
161
509
  }
162
510
  });
163
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
164
- getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
165
- strapi2.log.error(
166
- "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
167
- );
168
- throw err;
169
- });
170
- Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
171
- strapi2.webhookStore.addAllowedEvent(key, value);
172
- });
173
- }
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
+ });
174
522
  }
175
523
  };
176
524
  const destroy = async ({ strapi: strapi2 }) => {
177
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
178
- const scheduledJobs = getService("scheduling", {
179
- strapi: strapi2
180
- }).getAll();
181
- for (const [, job] of scheduledJobs) {
182
- job.cancel();
183
- }
525
+ const scheduledJobs = getService("scheduling", {
526
+ strapi: strapi2
527
+ }).getAll();
528
+ for (const [, job] of scheduledJobs) {
529
+ job.cancel();
184
530
  }
185
531
  };
186
532
  const schema$1 = {
@@ -215,6 +561,11 @@ const schema$1 = {
215
561
  timezone: {
216
562
  type: "string"
217
563
  },
564
+ status: {
565
+ type: "enumeration",
566
+ enum: ["ready", "blocked", "failed", "done", "empty"],
567
+ required: true
568
+ },
218
569
  actions: {
219
570
  type: "relation",
220
571
  relation: "oneToMany",
@@ -250,15 +601,13 @@ const schema = {
250
601
  enum: ["publish", "unpublish"],
251
602
  required: true
252
603
  },
253
- entry: {
254
- type: "relation",
255
- relation: "morphToOne",
256
- configurable: false
257
- },
258
604
  contentType: {
259
605
  type: "string",
260
606
  required: true
261
607
  },
608
+ entryDocumentId: {
609
+ type: "string"
610
+ },
262
611
  locale: {
263
612
  type: "string"
264
613
  },
@@ -267,6 +616,9 @@ const schema = {
267
616
  relation: "manyToOne",
268
617
  target: RELEASE_MODEL_UID,
269
618
  inversedBy: "actions"
619
+ },
620
+ isEntryValid: {
621
+ type: "boolean"
270
622
  }
271
623
  }
272
624
  };
@@ -277,18 +629,6 @@ const contentTypes = {
277
629
  release: release$1,
278
630
  "release-action": releaseAction$1
279
631
  };
280
- const getGroupName = (queryValue) => {
281
- switch (queryValue) {
282
- case "contentType":
283
- return "contentType.displayName";
284
- case "action":
285
- return "type";
286
- case "locale":
287
- return ___default.default.getOr("No locale", "locale.name");
288
- default:
289
- return "contentType.displayName";
290
- }
291
- };
292
632
  const createReleaseService = ({ strapi: strapi2 }) => {
293
633
  const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
294
634
  strapi2.eventHub.emit(event, {
@@ -297,6 +637,33 @@ const createReleaseService = ({ strapi: strapi2 }) => {
297
637
  release: release2
298
638
  });
299
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
+ };
300
667
  return {
301
668
  async create(releaseData, { user }) {
302
669
  const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
@@ -310,10 +677,13 @@ const createReleaseService = ({ strapi: strapi2 }) => {
310
677
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
311
678
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
312
679
  ]);
313
- const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
314
- data: releaseWithCreatorFields
680
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).create({
681
+ data: {
682
+ ...releaseWithCreatorFields,
683
+ status: "empty"
684
+ }
315
685
  });
316
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
686
+ if (releaseWithCreatorFields.scheduledAt) {
317
687
  const schedulingService = getService("scheduling", { strapi: strapi2 });
318
688
  await schedulingService.set(release2.id, release2.scheduledAt);
319
689
  }
@@ -321,94 +691,28 @@ const createReleaseService = ({ strapi: strapi2 }) => {
321
691
  return release2;
322
692
  },
323
693
  async findOne(id, query = {}) {
324
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
325
- ...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 }
326
698
  });
327
699
  return release2;
328
700
  },
329
701
  findPage(query) {
330
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
331
- ...query,
702
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
703
+ return strapi2.db.query(RELEASE_MODEL_UID).findPage({
704
+ ...dbQuery,
332
705
  populate: {
333
706
  actions: {
334
- // @ts-expect-error Ignore missing properties
335
707
  count: true
336
708
  }
337
709
  }
338
710
  });
339
711
  },
340
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
341
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
342
- where: {
343
- actions: {
344
- target_type: contentTypeUid,
345
- target_id: entryId
346
- },
347
- releasedAt: {
348
- $null: true
349
- }
350
- },
351
- populate: {
352
- // Filter the action to get only the content type entry
353
- actions: {
354
- where: {
355
- target_type: contentTypeUid,
356
- target_id: entryId
357
- }
358
- }
359
- }
360
- });
361
- return releases.map((release2) => {
362
- if (release2.actions?.length) {
363
- const [actionForEntry] = release2.actions;
364
- delete release2.actions;
365
- return {
366
- ...release2,
367
- action: actionForEntry
368
- };
369
- }
370
- return release2;
371
- });
372
- },
373
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
374
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
375
- where: {
376
- releasedAt: {
377
- $null: true
378
- },
379
- actions: {
380
- target_type: contentTypeUid,
381
- target_id: entryId
382
- }
383
- }
384
- });
385
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
386
- where: {
387
- $or: [
388
- {
389
- id: {
390
- $notIn: releasesRelated.map((release2) => release2.id)
391
- }
392
- },
393
- {
394
- actions: null
395
- }
396
- ],
397
- releasedAt: {
398
- $null: true
399
- }
400
- }
401
- });
402
- return releases.map((release2) => {
403
- if (release2.actions?.length) {
404
- const [actionForEntry] = release2.actions;
405
- delete release2.actions;
406
- return {
407
- ...release2,
408
- action: actionForEntry
409
- };
410
- }
411
- 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
412
716
  });
413
717
  },
414
718
  async update(id, releaseData, { user }) {
@@ -423,151 +727,27 @@ const createReleaseService = ({ strapi: strapi2 }) => {
423
727
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
424
728
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
425
729
  ]);
426
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
730
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id } });
427
731
  if (!release2) {
428
732
  throw new utils.errors.NotFoundError(`No release found for id ${id}`);
429
733
  }
430
734
  if (release2.releasedAt) {
431
735
  throw new utils.errors.ValidationError("Release already published");
432
736
  }
433
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
434
- /*
435
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
436
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
437
- */
438
- // @ts-expect-error see above
737
+ const updatedRelease = await strapi2.db.query(RELEASE_MODEL_UID).update({
738
+ where: { id },
439
739
  data: releaseWithCreatorFields
440
740
  });
441
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
442
- const schedulingService = getService("scheduling", { strapi: strapi2 });
443
- if (releaseData.scheduledAt) {
444
- await schedulingService.set(id, releaseData.scheduledAt);
445
- } else if (release2.scheduledAt) {
446
- schedulingService.cancel(id);
447
- }
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);
448
746
  }
747
+ this.updateReleaseStatus(id);
449
748
  strapi2.telemetry.send("didUpdateContentRelease");
450
749
  return updatedRelease;
451
750
  },
452
- async createAction(releaseId, action) {
453
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
454
- strapi: strapi2
455
- });
456
- await Promise.all([
457
- validateEntryContentType(action.entry.contentType),
458
- validateUniqueEntry(releaseId, action)
459
- ]);
460
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
461
- if (!release2) {
462
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
463
- }
464
- if (release2.releasedAt) {
465
- throw new utils.errors.ValidationError("Release already published");
466
- }
467
- const { entry, type } = action;
468
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
469
- data: {
470
- type,
471
- contentType: entry.contentType,
472
- locale: entry.locale,
473
- entry: {
474
- id: entry.id,
475
- __type: entry.contentType,
476
- __pivot: { field: "entry" }
477
- },
478
- release: releaseId
479
- },
480
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
481
- });
482
- },
483
- async findActions(releaseId, query) {
484
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
485
- fields: ["id"]
486
- });
487
- if (!release2) {
488
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
489
- }
490
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
491
- ...query,
492
- populate: {
493
- entry: {
494
- populate: "*"
495
- }
496
- },
497
- filters: {
498
- release: releaseId
499
- }
500
- });
501
- },
502
- async countActions(query) {
503
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
504
- },
505
- async groupActions(actions, groupBy) {
506
- const contentTypeUids = actions.reduce((acc, action) => {
507
- if (!acc.includes(action.contentType)) {
508
- acc.push(action.contentType);
509
- }
510
- return acc;
511
- }, []);
512
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
513
- contentTypeUids
514
- );
515
- const allLocalesDictionary = await this.getLocalesDataForActions();
516
- const formattedData = actions.map((action) => {
517
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
518
- return {
519
- ...action,
520
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
521
- contentType: {
522
- displayName,
523
- mainFieldValue: action.entry[mainField],
524
- uid: action.contentType
525
- }
526
- };
527
- });
528
- const groupName = getGroupName(groupBy);
529
- return ___default.default.groupBy(groupName)(formattedData);
530
- },
531
- async getLocalesDataForActions() {
532
- if (!strapi2.plugin("i18n")) {
533
- return {};
534
- }
535
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
536
- return allLocales.reduce((acc, locale) => {
537
- acc[locale.code] = { name: locale.name, code: locale.code };
538
- return acc;
539
- }, {});
540
- },
541
- async getContentTypesDataForActions(contentTypesUids) {
542
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
543
- const contentTypesData = {};
544
- for (const contentTypeUid of contentTypesUids) {
545
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
546
- uid: contentTypeUid
547
- });
548
- contentTypesData[contentTypeUid] = {
549
- mainField: contentTypeConfig.settings.mainField,
550
- displayName: strapi2.getModel(contentTypeUid).info.displayName
551
- };
552
- }
553
- return contentTypesData;
554
- },
555
- getContentTypeModelsFromActions(actions) {
556
- const contentTypeUids = actions.reduce((acc, action) => {
557
- if (!acc.includes(action.contentType)) {
558
- acc.push(action.contentType);
559
- }
560
- return acc;
561
- }, []);
562
- const contentTypeModelsMap = contentTypeUids.reduce(
563
- (acc, contentTypeUid) => {
564
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
565
- return acc;
566
- },
567
- {}
568
- );
569
- return contentTypeModelsMap;
570
- },
571
751
  async getAllComponents() {
572
752
  const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
573
753
  const components = await contentManagerComponentsService.findAllComponents();
@@ -581,10 +761,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
581
761
  return componentsMap;
582
762
  },
583
763
  async delete(releaseId) {
584
- 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 },
585
766
  populate: {
586
767
  actions: {
587
- fields: ["id"]
768
+ select: ["id"]
588
769
  }
589
770
  }
590
771
  });
@@ -602,9 +783,13 @@ const createReleaseService = ({ strapi: strapi2 }) => {
602
783
  }
603
784
  }
604
785
  });
605
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
786
+ await strapi2.db.query(RELEASE_MODEL_UID).delete({
787
+ where: {
788
+ id: releaseId
789
+ }
790
+ });
606
791
  });
607
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
792
+ if (release2.scheduledAt) {
608
793
  const schedulingService = getService("scheduling", { strapi: strapi2 });
609
794
  await schedulingService.cancel(release2.id);
610
795
  }
@@ -612,142 +797,294 @@ const createReleaseService = ({ strapi: strapi2 }) => {
612
797
  return release2;
613
798
  },
614
799
  async publish(releaseId) {
615
- try {
616
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
617
- RELEASE_MODEL_UID,
618
- releaseId,
619
- {
620
- populate: {
621
- actions: {
622
- populate: {
623
- entry: {
624
- fields: ["id"]
625
- }
626
- }
627
- }
628
- }
629
- }
630
- );
631
- 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) {
632
806
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
633
807
  }
634
- if (releaseWithPopulatedActionEntries.releasedAt) {
808
+ if (lockedRelease.releasedAt) {
635
809
  throw new utils.errors.ValidationError("Release already published");
636
810
  }
637
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
638
- throw new utils.errors.ValidationError("No entries to publish");
811
+ if (lockedRelease.status === "failed") {
812
+ throw new utils.errors.ValidationError("Release failed to publish");
639
813
  }
640
- const collectionTypeActions = {};
641
- const singleTypeActions = [];
642
- for (const action of releaseWithPopulatedActionEntries.actions) {
643
- const contentTypeUid = action.contentType;
644
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
645
- if (!collectionTypeActions[contentTypeUid]) {
646
- collectionTypeActions[contentTypeUid] = {
647
- entriestoPublishIds: [],
648
- entriesToUnpublishIds: []
649
- };
650
- }
651
- if (action.type === "publish") {
652
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
653
- } else {
654
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
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()
655
836
  }
656
- } else {
657
- singleTypeActions.push({
658
- uid: contentTypeUid,
659
- action: action.type,
660
- id: action.entry.id
661
- });
662
- }
837
+ });
838
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
839
+ isPublished: true,
840
+ release: release22
841
+ });
842
+ strapi2.telemetry.send("didPublishContentRelease");
843
+ return { release: release22, error: null };
844
+ } catch (error2) {
845
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
846
+ isPublished: false,
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
+ };
663
856
  }
664
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
665
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
666
- await strapi2.db.transaction(async () => {
667
- for (const { uid, action, id } of singleTypeActions) {
668
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
669
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
670
- try {
671
- if (action === "publish") {
672
- await entityManagerService.publish(entry, uid);
673
- } else {
674
- await entityManagerService.unpublish(entry, uid);
675
- }
676
- } catch (error) {
677
- if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft")) {
678
- } else {
679
- throw error;
680
- }
681
- }
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
682
869
  }
683
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
684
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
685
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
686
- const entriesToPublish = await strapi2.entityService.findMany(
687
- contentTypeUid,
688
- {
689
- filters: {
690
- id: {
691
- $in: entriestoPublishIds
692
- }
693
- },
694
- populate
695
- }
696
- );
697
- const entriesToUnpublish = await strapi2.entityService.findMany(
698
- contentTypeUid,
699
- {
700
- filters: {
701
- id: {
702
- $in: entriesToUnpublishIds
703
- }
704
- },
705
- populate
706
- }
707
- );
708
- if (entriesToPublish.length > 0) {
709
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
710
- }
711
- if (entriesToUnpublish.length > 0) {
712
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
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"
713
886
  }
887
+ });
888
+ }
889
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
890
+ where: {
891
+ id: releaseId
892
+ },
893
+ data: {
894
+ status: "ready"
714
895
  }
715
896
  });
716
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
717
- data: {
718
- /*
719
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
720
- */
721
- // @ts-expect-error see above
722
- releasedAt: /* @__PURE__ */ new Date()
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"
723
1018
  },
724
- populate: {
725
- actions: {
726
- // @ts-expect-error is not expecting count but it is working
727
- count: true
728
- }
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
729
1050
  }
730
- });
731
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
732
- dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
733
- isPublished: true,
734
- release: release2
735
- });
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);
736
1060
  }
737
- strapi2.telemetry.send("didPublishContentRelease");
738
- return release2;
739
- } catch (error) {
740
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
741
- dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
742
- isPublished: false,
743
- error
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"
744
1070
  });
745
- }
746
- throw error;
747
- }
1071
+ acc[contentTypeUid] = {
1072
+ ...contentTypeModel,
1073
+ hasReviewWorkflow: !!workflow,
1074
+ stageRequiredToPublish: workflow?.stageRequiredToPublish
1075
+ };
1076
+ return acc;
1077
+ },
1078
+ {}
1079
+ );
1080
+ return contentTypeModelsMap;
748
1081
  },
749
- async updateAction(actionId, releaseId, update) {
750
- 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({
751
1088
  where: {
752
1089
  id: actionId,
753
1090
  release: {
@@ -756,17 +1093,42 @@ const createReleaseService = ({ strapi: strapi2 }) => {
756
1093
  $null: true
757
1094
  }
758
1095
  }
759
- },
760
- data: update
1096
+ }
761
1097
  });
762
- if (!updatedAction) {
1098
+ if (!action) {
763
1099
  throw new utils.errors.NotFoundError(
764
1100
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
765
1101
  );
766
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);
767
1129
  return updatedAction;
768
1130
  },
769
- async deleteAction(actionId, releaseId) {
1131
+ async delete(actionId, releaseId) {
770
1132
  const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
771
1133
  where: {
772
1134
  id: actionId,
@@ -783,43 +1145,104 @@ const createReleaseService = ({ strapi: strapi2 }) => {
783
1145
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
784
1146
  );
785
1147
  }
1148
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
786
1149
  return deletedAction;
1150
+ },
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
1162
+ }
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({
1178
+ where: {
1179
+ id: action.id
1180
+ },
1181
+ data: {
1182
+ isEntryValid: isValid
1183
+ }
1184
+ });
1185
+ if (!releasesUpdated.includes(action.release.id)) {
1186
+ releasesUpdated.push(action.release.id);
1187
+ }
1188
+ return {
1189
+ id: action.id,
1190
+ isEntryValid: isValid
1191
+ };
1192
+ });
1193
+ if (releasesUpdated.length > 0) {
1194
+ await utils.async.map(releasesUpdated, async (releaseId) => {
1195
+ await getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1196
+ });
1197
+ }
787
1198
  }
788
1199
  };
789
1200
  };
1201
+ class AlreadyOnReleaseError extends utils.errors.ApplicationError {
1202
+ constructor(message) {
1203
+ super(message);
1204
+ this.name = "AlreadyOnReleaseError";
1205
+ }
1206
+ }
790
1207
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
791
1208
  async validateUniqueEntry(releaseId, releaseActionArgs) {
792
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
793
- 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
+ }
794
1216
  });
795
1217
  if (!release2) {
796
1218
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
797
1219
  }
798
1220
  const isEntryInRelease = release2.actions.some(
799
- (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)
800
1222
  );
801
1223
  if (isEntryInRelease) {
802
- throw new utils.errors.ValidationError(
803
- `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}`
804
1226
  );
805
1227
  }
806
1228
  },
807
- validateEntryContentType(contentTypeUid) {
1229
+ validateEntryData(contentTypeUid, entryDocumentId) {
808
1230
  const contentType = strapi2.contentType(contentTypeUid);
809
1231
  if (!contentType) {
810
1232
  throw new utils.errors.NotFoundError(`No content type found for uid ${contentTypeUid}`);
811
1233
  }
812
- if (!contentType.options?.draftAndPublish) {
1234
+ if (!utils.contentTypes.hasDraftAndPublish(contentType)) {
813
1235
  throw new utils.errors.ValidationError(
814
1236
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
815
1237
  );
816
1238
  }
1239
+ if (contentType.kind === "collectionType" && !entryDocumentId) {
1240
+ throw new utils.errors.ValidationError("Document id is required for collection type");
1241
+ }
817
1242
  },
818
1243
  async validatePendingReleasesLimit() {
819
- const maximumPendingReleases = (
820
- // @ts-expect-error - options is not typed into features
821
- EE__default.default.features.get("cms-content-releases")?.options?.maximumReleases || 3
822
- );
1244
+ const featureCfg = strapi2.ee.features.get("cms-content-releases");
1245
+ const maximumPendingReleases = typeof featureCfg === "object" && featureCfg?.options?.maximumReleases || 3;
823
1246
  const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
824
1247
  filters: {
825
1248
  releasedAt: {
@@ -832,8 +1255,8 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
832
1255
  }
833
1256
  },
834
1257
  async validateUniqueNameForPendingRelease(name, id) {
835
- const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
836
- filters: {
1258
+ const pendingReleases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1259
+ where: {
837
1260
  releasedAt: {
838
1261
  $null: true
839
1262
  },
@@ -862,7 +1285,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
862
1285
  }
863
1286
  const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
864
1287
  try {
865
- await getService("release").publish(releaseId);
1288
+ await getService("release", { strapi: strapi2 }).publish(releaseId);
866
1289
  } catch (error) {
867
1290
  }
868
1291
  this.cancel(releaseId);
@@ -904,80 +1327,172 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
904
1327
  }
905
1328
  };
906
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
+ };
907
1351
  const services = {
908
1352
  release: createReleaseService,
1353
+ "release-action": createReleaseActionService,
909
1354
  "release-validation": createReleaseValidationService,
910
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1355
+ scheduling: createSchedulingService,
1356
+ settings: createSettingsService
911
1357
  };
912
- const RELEASE_SCHEMA = yup__namespace.object().shape({
913
- name: yup__namespace.string().trim().required(),
914
- scheduledAt: yup__namespace.string().nullable(),
915
- isScheduled: yup__namespace.boolean().optional(),
916
- time: yup__namespace.string().when("isScheduled", {
917
- is: true,
918
- then: yup__namespace.string().trim().required(),
919
- otherwise: yup__namespace.string().nullable()
920
- }),
921
- timezone: yup__namespace.string().when("isScheduled", {
922
- is: true,
923
- then: yup__namespace.string().required().nullable(),
924
- otherwise: yup__namespace.string().nullable()
925
- }),
926
- date: yup__namespace.string().when("isScheduled", {
927
- is: true,
928
- then: yup__namespace.string().required().nullable(),
929
- 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()
930
1365
  })
931
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();
932
1373
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
1374
+ const validatefindByDocumentAttachedParams = utils.validateYupSchema(
1375
+ FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA
1376
+ );
933
1377
  const releaseController = {
934
- async findMany(ctx) {
935
- 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({
936
1385
  ability: ctx.state.userAbility,
937
1386
  model: RELEASE_MODEL_UID
938
1387
  });
939
1388
  await permissionsManager.validateQuery(ctx.query);
940
1389
  const releaseService = getService("release", { strapi });
941
- const isFindManyForContentTypeEntry = Boolean(ctx.query?.contentTypeUid && ctx.query?.entryId);
942
- if (isFindManyForContentTypeEntry) {
943
- const query = await permissionsManager.sanitizeQuery(ctx.query);
944
- const contentTypeUid = query.contentTypeUid;
945
- const entryId = query.entryId;
946
- const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
947
- const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
948
- ctx.body = { data };
949
- } else {
950
- const query = await permissionsManager.sanitizeQuery(ctx.query);
951
- const { results, pagination } = await releaseService.findPage(query);
952
- const data = results.map((release2) => {
953
- const { actions, ...releaseData } = release2;
954
- return {
955
- ...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,
1406
+ actions: {
1407
+ contentType,
1408
+ entryDocumentId: entryDocumentId ?? null,
1409
+ locale: locale ?? null
1410
+ }
1411
+ },
1412
+ populate: {
956
1413
  actions: {
957
- meta: {
958
- count: actions.count
1414
+ fields: ["type"],
1415
+ filters: {
1416
+ contentType,
1417
+ entryDocumentId: entryDocumentId ?? null,
1418
+ locale: locale ?? null
959
1419
  }
960
1420
  }
961
- };
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
+ }
1434
+ });
1435
+ const releases = await releaseService.findMany({
1436
+ where: {
1437
+ $or: [
1438
+ {
1439
+ id: {
1440
+ $notIn: relatedReleases.map((release2) => release2.id)
1441
+ }
1442
+ },
1443
+ {
1444
+ actions: null
1445
+ }
1446
+ ],
1447
+ releasedAt: null
1448
+ }
962
1449
  });
963
- ctx.body = { data, meta: { pagination } };
1450
+ ctx.body = { data: releases };
964
1451
  }
965
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
+ },
966
1480
  async findOne(ctx) {
967
1481
  const id = ctx.params.id;
968
1482
  const releaseService = getService("release", { strapi });
1483
+ const releaseActionService = getService("release-action", { strapi });
969
1484
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
970
1485
  if (!release2) {
971
1486
  throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
972
1487
  }
973
- const count = await releaseService.countActions({
1488
+ const count = await releaseActionService.countActions({
974
1489
  filters: {
975
1490
  release: id
976
1491
  }
977
1492
  });
978
1493
  const sanitizedRelease = {
979
1494
  ...release2,
980
- createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1495
+ createdBy: release2.createdBy ? strapi.service("admin::user").sanitizeUser(release2.createdBy) : null
981
1496
  };
982
1497
  const data = {
983
1498
  ...sanitizedRelease,
@@ -989,19 +1504,63 @@ const releaseController = {
989
1504
  };
990
1505
  ctx.body = { data };
991
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
+ },
992
1551
  async create(ctx) {
993
1552
  const user = ctx.state.user;
994
1553
  const releaseArgs = ctx.request.body;
995
1554
  await validateRelease(releaseArgs);
996
1555
  const releaseService = getService("release", { strapi });
997
1556
  const release2 = await releaseService.create(releaseArgs, { user });
998
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1557
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
999
1558
  ability: ctx.state.userAbility,
1000
1559
  model: RELEASE_MODEL_UID
1001
1560
  });
1002
- ctx.body = {
1561
+ ctx.created({
1003
1562
  data: await permissionsManager.sanitizeOutput(release2)
1004
- };
1563
+ });
1005
1564
  },
1006
1565
  async update(ctx) {
1007
1566
  const user = ctx.state.user;
@@ -1010,7 +1569,7 @@ const releaseController = {
1010
1569
  await validateRelease(releaseArgs);
1011
1570
  const releaseService = getService("release", { strapi });
1012
1571
  const release2 = await releaseService.update(id, releaseArgs, { user });
1013
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1572
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1014
1573
  ability: ctx.state.userAbility,
1015
1574
  model: RELEASE_MODEL_UID
1016
1575
  });
@@ -1027,18 +1586,18 @@ const releaseController = {
1027
1586
  };
1028
1587
  },
1029
1588
  async publish(ctx) {
1030
- const user = ctx.state.user;
1031
1589
  const id = ctx.params.id;
1032
1590
  const releaseService = getService("release", { strapi });
1033
- const release2 = await releaseService.publish(id, { user });
1591
+ const releaseActionService = getService("release-action", { strapi });
1592
+ const release2 = await releaseService.publish(id);
1034
1593
  const [countPublishActions, countUnpublishActions] = await Promise.all([
1035
- releaseService.countActions({
1594
+ releaseActionService.countActions({
1036
1595
  filters: {
1037
1596
  release: id,
1038
1597
  type: "publish"
1039
1598
  }
1040
1599
  }),
1041
- releaseService.countActions({
1600
+ releaseActionService.countActions({
1042
1601
  filters: {
1043
1602
  release: id,
1044
1603
  type: "unpublish"
@@ -1056,57 +1615,106 @@ const releaseController = {
1056
1615
  }
1057
1616
  };
1058
1617
  const RELEASE_ACTION_SCHEMA = utils.yup.object().shape({
1059
- entry: utils.yup.object().shape({
1060
- id: utils.yup.strapiID().required(),
1061
- contentType: utils.yup.string().required()
1062
- }).required(),
1618
+ contentType: utils.yup.string().required(),
1619
+ entryDocumentId: utils.yup.strapiID(),
1620
+ locale: utils.yup.string(),
1063
1621
  type: utils.yup.string().oneOf(["publish", "unpublish"]).required()
1064
1622
  });
1065
1623
  const RELEASE_ACTION_UPDATE_SCHEMA = utils.yup.object().shape({
1066
1624
  type: utils.yup.string().oneOf(["publish", "unpublish"]).required()
1067
1625
  });
1626
+ const FIND_MANY_ACTIONS_PARAMS = utils.yup.object().shape({
1627
+ groupBy: utils.yup.string().oneOf(["action", "contentType", "locale"])
1628
+ });
1068
1629
  const validateReleaseAction = utils.validateYupSchema(RELEASE_ACTION_SCHEMA);
1069
1630
  const validateReleaseActionUpdateSchema = utils.validateYupSchema(RELEASE_ACTION_UPDATE_SCHEMA);
1631
+ const validateFindManyActionsParams = utils.validateYupSchema(FIND_MANY_ACTIONS_PARAMS);
1070
1632
  const releaseActionController = {
1071
1633
  async create(ctx) {
1072
1634
  const releaseId = ctx.params.releaseId;
1073
1635
  const releaseActionArgs = ctx.request.body;
1074
1636
  await validateReleaseAction(releaseActionArgs);
1075
- const releaseService = getService("release", { strapi });
1076
- const releaseAction2 = await releaseService.createAction(releaseId, releaseActionArgs);
1077
- ctx.body = {
1637
+ const releaseActionService = getService("release-action", { strapi });
1638
+ const releaseAction2 = await releaseActionService.create(releaseId, releaseActionArgs);
1639
+ ctx.created({
1078
1640
  data: releaseAction2
1079
- };
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
+ });
1080
1680
  },
1081
1681
  async findMany(ctx) {
1082
1682
  const releaseId = ctx.params.releaseId;
1083
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1683
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1084
1684
  ability: ctx.state.userAbility,
1085
1685
  model: RELEASE_ACTION_MODEL_UID
1086
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;
1087
1695
  const query = await permissionsManager.sanitizeQuery(ctx.query);
1088
- const releaseService = getService("release", { strapi });
1089
- const { results, pagination } = await releaseService.findActions(releaseId, {
1090
- sort: query.groupBy === "action" ? "type" : query.groupBy,
1696
+ const releaseActionService = getService("release-action", { strapi });
1697
+ const { results, pagination } = await releaseActionService.findPage(releaseId, {
1091
1698
  ...query
1092
1699
  });
1093
1700
  const contentTypeOutputSanitizers = results.reduce((acc, action) => {
1094
1701
  if (acc[action.contentType]) {
1095
1702
  return acc;
1096
1703
  }
1097
- const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
1704
+ const contentTypePermissionsManager = strapi.service("admin::permission").createPermissionsManager({
1098
1705
  ability: ctx.state.userAbility,
1099
1706
  model: action.contentType
1100
1707
  });
1101
1708
  acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
1102
1709
  return acc;
1103
1710
  }, {});
1104
- const sanitizedResults = await utils.mapAsync(results, async (action) => ({
1711
+ const sanitizedResults = await utils.async.map(results, async (action) => ({
1105
1712
  ...action,
1106
- entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1713
+ entry: action.entry ? await contentTypeOutputSanitizers[action.contentType](action.entry) : {}
1107
1714
  }));
1108
- const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
1109
- 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 });
1110
1718
  const components = await releaseService.getAllComponents();
1111
1719
  ctx.body = {
1112
1720
  data: groupedData,
@@ -1122,8 +1730,8 @@ const releaseActionController = {
1122
1730
  const releaseId = ctx.params.releaseId;
1123
1731
  const releaseActionUpdateArgs = ctx.request.body;
1124
1732
  await validateReleaseActionUpdateSchema(releaseActionUpdateArgs);
1125
- const releaseService = getService("release", { strapi });
1126
- const updatedAction = await releaseService.updateAction(
1733
+ const releaseActionService = getService("release-action", { strapi });
1734
+ const updatedAction = await releaseActionService.update(
1127
1735
  actionId,
1128
1736
  releaseId,
1129
1737
  releaseActionUpdateArgs
@@ -1135,17 +1743,71 @@ const releaseActionController = {
1135
1743
  async delete(ctx) {
1136
1744
  const actionId = ctx.params.actionId;
1137
1745
  const releaseId = ctx.params.releaseId;
1138
- const releaseService = getService("release", { strapi });
1139
- const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
1746
+ const releaseActionService = getService("release-action", { strapi });
1747
+ const deletedReleaseAction = await releaseActionService.delete(actionId, releaseId);
1140
1748
  ctx.body = {
1141
1749
  data: deletedReleaseAction
1142
1750
  };
1143
1751
  }
1144
1752
  };
1145
- 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
+ };
1146
1776
  const release = {
1147
1777
  type: "admin",
1148
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
+ },
1149
1811
  {
1150
1812
  method: "POST",
1151
1813
  path: "/",
@@ -1165,7 +1827,7 @@ const release = {
1165
1827
  {
1166
1828
  method: "GET",
1167
1829
  path: "/",
1168
- handler: "release.findMany",
1830
+ handler: "release.findPage",
1169
1831
  config: {
1170
1832
  policies: [
1171
1833
  "admin::isAuthenticatedAdmin",
@@ -1263,6 +1925,22 @@ const releaseAction = {
1263
1925
  ]
1264
1926
  }
1265
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
+ },
1266
1944
  {
1267
1945
  method: "GET",
1268
1946
  path: "/:releaseId/actions",
@@ -1313,13 +1991,50 @@ const releaseAction = {
1313
1991
  }
1314
1992
  ]
1315
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
+ };
1316
2031
  const routes = {
2032
+ settings,
1317
2033
  release,
1318
2034
  "release-action": releaseAction
1319
2035
  };
1320
- const { features } = require("@strapi/strapi/dist/utils/ee");
1321
2036
  const getPlugin = () => {
1322
- if (features.isEnabled("cms-content-releases")) {
2037
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
1323
2038
  return {
1324
2039
  register,
1325
2040
  bootstrap,
@@ -1331,6 +2046,9 @@ const getPlugin = () => {
1331
2046
  };
1332
2047
  }
1333
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
1334
2052
  contentTypes
1335
2053
  };
1336
2054
  };