@strapi/content-releases 0.0.0-next.6d59515520a3850456f256fb0e4c54b75054ddf4 → 0.0.0-next.7051057497777a23c7418e675330d2fb69c89fec

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