@strapi/content-releases 0.0.0-next.836f74517f9a428a4798ed889c3f05057ec6beb1 → 0.0.0-next.840550dc97a3782302ddf918d3a0d07e59dd11eb

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