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