@strapi/content-releases 0.0.0-experimental.ee4d311a5e6a131fad03cf07e4696f49fdd9c2e6 → 0.0.0-experimental.f0a0bc26f5ef0693aaea2a616bc6b816cfee54b6

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-FQyYFBJT.mjs +1559 -0
  3. package/dist/_chunks/App-FQyYFBJT.mjs.map +1 -0
  4. package/dist/_chunks/App-lx4Ucy9W.js +1580 -0
  5. package/dist/_chunks/App-lx4Ucy9W.js.map +1 -0
  6. package/dist/_chunks/PurchaseContentReleases-Be3acS2L.js +52 -0
  7. package/dist/_chunks/PurchaseContentReleases-Be3acS2L.js.map +1 -0
  8. package/dist/_chunks/PurchaseContentReleases-_MxP6-Dt.mjs +52 -0
  9. package/dist/_chunks/PurchaseContentReleases-_MxP6-Dt.mjs.map +1 -0
  10. package/dist/_chunks/ReleasesSettingsPage-DqBxvJ9i.mjs +178 -0
  11. package/dist/_chunks/ReleasesSettingsPage-DqBxvJ9i.mjs.map +1 -0
  12. package/dist/_chunks/ReleasesSettingsPage-T5VEAV03.js +178 -0
  13. package/dist/_chunks/ReleasesSettingsPage-T5VEAV03.js.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-CK9G80CL.mjs +1380 -0
  19. package/dist/_chunks/index-CK9G80CL.mjs.map +1 -0
  20. package/dist/_chunks/index-Cl3tM1YW.js +1399 -0
  21. package/dist/_chunks/index-Cl3tM1YW.js.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/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 +1456 -485
  57. package/dist/server/index.js.map +1 -1
  58. package/dist/server/index.mjs +1456 -485
  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 -37
  131. package/dist/_chunks/App-0yPbcoGt.js +0 -1037
  132. package/dist/_chunks/App-0yPbcoGt.js.map +0 -1
  133. package/dist/_chunks/App-BWaM2ihP.mjs +0 -1015
  134. package/dist/_chunks/App-BWaM2ihP.mjs.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-EIe8S-cw.mjs +0 -887
  138. package/dist/_chunks/index-EIe8S-cw.mjs.map +0 -1
  139. package/dist/_chunks/index-l5iuP0Hb.js +0 -908
  140. package/dist/_chunks/index-l5iuP0Hb.js.map +0 -1
  141. package/strapi-server.js +0 -3
@@ -1,6 +1,8 @@
1
- import { setCreatorFields, errors, validateYupSchema, yup as yup$1, mapAsync } 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";
3
+ import { difference, keys } from "lodash";
2
4
  import _ from "lodash/fp";
3
- import EE from "@strapi/strapi/dist/utils/ee";
5
+ import { scheduleJob } from "node-schedule";
4
6
  import * as yup from "yup";
5
7
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
6
8
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -46,82 +48,463 @@ const ACTIONS = [
46
48
  displayName: "Add an entry to a release",
47
49
  uid: "create-action",
48
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"
49
68
  }
50
69
  ];
51
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
70
+ const ALLOWED_WEBHOOK_EVENTS = {
71
+ RELEASES_PUBLISH: "releases.publish"
72
+ };
73
+ const getService = (name, { strapi: strapi2 }) => {
52
74
  return strapi2.plugin("content-releases").service(name);
53
75
  };
54
- const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
55
- const register = async ({ strapi: strapi2 }) => {
56
- if (features$2.isEnabled("cms-content-releases")) {
57
- await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
58
- const releaseActionService = getService("release-action", { strapi: strapi2 });
59
- const eventManager = getService("event-manager", { strapi: strapi2 });
60
- const destroyContentTypeUpdateListener = strapi2.eventHub.on(
61
- "content-type.update",
62
- async ({ contentType }) => {
63
- if (contentType.schema?.options?.draftAndPublish === false) {
64
- await releaseActionService.deleteManyForContentType(contentType.uid);
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
+ };
139
+ async function deleteActionsOnDisableDraftAndPublish({
140
+ oldContentTypes,
141
+ contentTypes: contentTypes2
142
+ }) {
143
+ if (!oldContentTypes) {
144
+ return;
145
+ }
146
+ for (const uid in contentTypes2) {
147
+ if (!oldContentTypes[uid]) {
148
+ continue;
149
+ }
150
+ const oldContentType = oldContentTypes[uid];
151
+ const contentType = contentTypes2[uid];
152
+ if (contentTypes$1.hasDraftAndPublish(oldContentType) && !contentTypes$1.hasDraftAndPublish(contentType)) {
153
+ await strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: uid }).execute();
154
+ }
155
+ }
156
+ }
157
+ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
158
+ const deletedContentTypes = difference(keys(oldContentTypes), keys(contentTypes2)) ?? [];
159
+ if (deletedContentTypes.length) {
160
+ await async.map(deletedContentTypes, async (deletedContentTypeUID) => {
161
+ return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
162
+ });
163
+ }
164
+ }
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
65
175
  }
66
176
  }
67
- );
68
- eventManager.addDestroyListenerCallback(destroyContentTypeUpdateListener);
69
- const destroyContentTypeDeleteListener = strapi2.eventHub.on(
70
- "content-type.delete",
71
- async ({ contentType }) => {
72
- await releaseActionService.deleteManyForContentType(contentType.uid);
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
73
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
74
227
  );
75
- eventManager.addDestroyListenerCallback(destroyContentTypeDeleteListener);
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
+ };
349
+ const register = async ({ strapi: strapi2 }) => {
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);
76
459
  }
77
460
  };
78
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
79
461
  const bootstrap = async ({ strapi: strapi2 }) => {
80
- 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
+ );
81
466
  strapi2.db.lifecycles.subscribe({
82
- afterDelete(event) {
83
- const { model, result } = event;
84
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
85
- const { id } = result;
86
- strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
87
- where: {
88
- target_type: model.uid,
89
- target_id: id
90
- }
91
- });
92
- }
93
- },
467
+ models: contentTypesWithDraftAndPublish,
94
468
  /**
95
- * deleteMany hook doesn't return the deleted entries ids
96
- * so we need to fetch them before deleting the entries to save the ids on our state
97
- */
98
- async beforeDeleteMany(event) {
99
- const { model, params } = event;
100
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
101
- const { where } = params;
102
- const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
103
- event.state.entriesToDelete = entriesToDelete;
104
- }
105
- },
106
- /**
107
- * We delete the release actions related to deleted entries
108
- * 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
109
470
  */
110
471
  async afterDeleteMany(event) {
111
- const { model, state } = event;
112
- const entriesToDelete = state.entriesToDelete;
113
- if (entriesToDelete) {
114
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
115
- where: {
116
- target_type: model.uid,
117
- target_id: {
118
- $in: entriesToDelete.map((entry) => entry.id)
119
- }
120
- }
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
121
485
  });
122
486
  }
123
487
  }
124
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();
125
508
  }
126
509
  };
127
510
  const schema$1 = {
@@ -150,6 +533,17 @@ const schema$1 = {
150
533
  releasedAt: {
151
534
  type: "datetime"
152
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
+ },
153
547
  actions: {
154
548
  type: "relation",
155
549
  relation: "oneToMany",
@@ -185,15 +579,13 @@ const schema = {
185
579
  enum: ["publish", "unpublish"],
186
580
  required: true
187
581
  },
188
- entry: {
189
- type: "relation",
190
- relation: "morphToOne",
191
- configurable: false
192
- },
193
582
  contentType: {
194
583
  type: "string",
195
584
  required: true
196
585
  },
586
+ entryDocumentId: {
587
+ type: "string"
588
+ },
197
589
  locale: {
198
590
  type: "string"
199
591
  },
@@ -202,6 +594,9 @@ const schema = {
202
594
  relation: "manyToOne",
203
595
  target: RELEASE_MODEL_UID,
204
596
  inversedBy: "actions"
597
+ },
598
+ isEntryValid: {
599
+ type: "boolean"
205
600
  }
206
601
  }
207
602
  };
@@ -212,210 +607,297 @@ const contentTypes = {
212
607
  release: release$1,
213
608
  "release-action": releaseAction$1
214
609
  };
215
- const createReleaseActionService = ({ strapi: strapi2 }) => ({
216
- async deleteManyForContentType(contentTypeUid) {
217
- return strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
218
- where: {
219
- target_type: contentTypeUid
220
- }
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
221
616
  });
222
- }
223
- });
224
- const getGroupName = (queryValue) => {
225
- switch (queryValue) {
226
- case "contentType":
227
- return "contentType.displayName";
228
- case "action":
229
- return "type";
230
- case "locale":
231
- return _.getOr("No locale", "locale.name");
232
- default:
233
- return "contentType.displayName";
234
- }
235
- };
236
- const createReleaseService = ({ strapi: strapi2 }) => ({
237
- async create(releaseData, { user }) {
238
- const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
239
- await getService("release-validation", { strapi: strapi2 }).validatePendingReleasesLimit();
240
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
241
- data: releaseWithCreatorFields
242
- });
243
- },
244
- async findOne(id, query = {}) {
245
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
246
- ...query
247
- });
248
- return release2;
249
- },
250
- findPage(query) {
251
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
252
- ...query,
253
- populate: {
254
- actions: {
255
- // @ts-expect-error Ignore missing properties
256
- 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
257
623
  }
258
624
  }
259
625
  });
260
- },
261
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
262
- hasEntryAttached
263
- } = {
264
- hasEntryAttached: false
265
- }) {
266
- const whereActions = hasEntryAttached ? {
267
- // Find all Releases where the content type entry is present
268
- actions: {
269
- target_type: contentTypeUid,
270
- target_id: entryId
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
+ };
271
637
  }
272
- } : {
273
- // Find all Releases where the content type entry is not present
274
- $or: [
275
- {
276
- $not: {
277
- actions: {
278
- target_type: contentTypeUid,
279
- target_id: entryId
280
- }
281
- }
282
- },
283
- {
284
- actions: null
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"
285
662
  }
286
- ]
287
- };
288
- const populateAttachedAction = hasEntryAttached ? {
289
- // Filter the action to get only the content type entry
290
- actions: {
291
- where: {
292
- target_type: contentTypeUid,
293
- 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
686
+ }
294
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}`);
295
711
  }
296
- } : {};
297
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
298
- where: {
299
- ...whereActions,
300
- releasedAt: {
301
- $null: true
712
+ if (release2.releasedAt) {
713
+ throw new errors.ValidationError("Release already published");
714
+ }
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;
736
+ },
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
+ }
302
748
  }
303
- },
304
- populate: {
305
- ...populateAttachedAction
749
+ });
750
+ if (!release2) {
751
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
306
752
  }
307
- });
308
- return releases.map((release2) => {
309
- if (release2.actions?.length) {
310
- const [actionForEntry] = release2.actions;
311
- delete release2.actions;
312
- return {
313
- ...release2,
314
- action: actionForEntry
315
- };
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: {
759
+ id: {
760
+ $in: release2.actions.map((action) => action.id)
761
+ }
762
+ }
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);
316
773
  }
774
+ strapi2.telemetry.send("didDeleteContentRelease");
317
775
  return release2;
318
- });
319
- },
320
- async update(id, releaseData, { user }) {
321
- const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(releaseData);
322
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
323
- if (!release2) {
324
- throw new errors.NotFoundError(`No release found for id ${id}`);
325
- }
326
- if (release2.releasedAt) {
327
- throw new errors.ValidationError("Release already published");
328
- }
329
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
330
- /*
331
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
332
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
333
- */
334
- // @ts-expect-error see above
335
- data: releaseWithCreatorFields
336
- });
337
- return updatedRelease;
338
- },
339
- async createAction(releaseId, action) {
340
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
341
- strapi: strapi2
342
- });
343
- await Promise.all([
344
- validateEntryContentType(action.entry.contentType),
345
- validateUniqueEntry(releaseId, action)
346
- ]);
347
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
348
- if (!release2) {
349
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
350
- }
351
- if (release2.releasedAt) {
352
- throw new errors.ValidationError("Release already published");
353
- }
354
- const { entry, type } = action;
355
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
356
- data: {
357
- type,
358
- contentType: entry.contentType,
359
- locale: entry.locale,
360
- entry: {
361
- id: entry.id,
362
- __type: entry.contentType,
363
- __pivot: { field: "entry" }
364
- },
365
- release: releaseId
366
- },
367
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
368
- });
369
- },
370
- async findActions(releaseId, query) {
371
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
372
- fields: ["id"]
373
- });
374
- if (!release2) {
375
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
376
- }
377
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
378
- ...query,
379
- populate: {
380
- entry: {
381
- 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}`);
382
785
  }
383
- },
384
- filters: {
385
- 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;
386
838
  }
387
- });
388
- },
389
- async countActions(query) {
390
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
391
- },
392
- async groupActions(actions, groupBy) {
393
- const contentTypeUids = actions.reduce((acc, action) => {
394
- if (!acc.includes(action.contentType)) {
395
- 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
+ });
396
875
  }
397
- return acc;
398
- }, []);
399
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
400
- contentTypeUids
401
- );
402
- const allLocalesDictionary = await this.getLocalesDataForActions();
403
- const formattedData = actions.map((action) => {
404
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
405
- return {
406
- ...action,
407
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
408
- contentType: {
409
- displayName,
410
- mainFieldValue: action.entry[mainField],
411
- uid: action.contentType
876
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
877
+ where: {
878
+ id: releaseId
879
+ },
880
+ data: {
881
+ status: "empty"
412
882
  }
413
- };
414
- });
415
- const groupName = getGroupName(groupBy);
416
- return _.groupBy(groupName)(formattedData);
417
- },
418
- 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 () => {
419
901
  if (!strapi2.plugin("i18n")) {
420
902
  return {};
421
903
  }
@@ -424,8 +906,8 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
424
906
  acc[locale.code] = { name: locale.name, code: locale.code };
425
907
  return acc;
426
908
  }, {});
427
- },
428
- async getContentTypesDataForActions(contentTypesUids) {
909
+ };
910
+ const getContentTypesDataForActions = async (contentTypesUids) => {
429
911
  const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
430
912
  const contentTypesData = {};
431
913
  for (const contentTypeUid of contentTypesUids) {
@@ -438,195 +920,307 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
438
920
  };
439
921
  }
440
922
  return contentTypesData;
441
- },
442
- getContentTypeModelsFromActions(actions) {
443
- const contentTypeUids = actions.reduce((acc, action) => {
444
- if (!acc.includes(action.contentType)) {
445
- acc.push(action.contentType);
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}`);
938
+ }
939
+ action.entryDocumentId = document.documentId;
446
940
  }
447
- return acc;
448
- }, []);
449
- const contentTypeModelsMap = contentTypeUids.reduce(
450
- (acc, contentTypeUid) => {
451
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
452
- return acc;
453
- },
454
- {}
455
- );
456
- return contentTypeModelsMap;
457
- },
458
- async getAllComponents() {
459
- const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
460
- const components = await contentManagerComponentsService.findAllComponents();
461
- const componentsMap = components.reduce(
462
- (acc, component) => {
463
- acc[component.uid] = component;
464
- return acc;
465
- },
466
- {}
467
- );
468
- return componentsMap;
469
- },
470
- async delete(releaseId) {
471
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
472
- populate: {
473
- actions: {
474
- fields: ["id"]
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
475
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"] } }
965
+ });
966
+ if (!disableUpdateReleaseStatus) {
967
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
476
968
  }
477
- });
478
- if (!release2) {
479
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
480
- }
481
- if (release2.releasedAt) {
482
- throw new errors.ValidationError("Release already published");
483
- }
484
- await strapi2.db.transaction(async () => {
485
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
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}`);
978
+ }
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,
486
982
  where: {
487
- id: {
488
- $in: release2.actions.map((action) => action.id)
489
- }
983
+ release: releaseId
490
984
  }
491
985
  });
492
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
493
- });
494
- return release2;
495
- },
496
- async publish(releaseId) {
497
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
498
- RELEASE_MODEL_UID,
499
- releaseId,
500
- {
501
- populate: {
502
- actions: {
503
- populate: {
504
- entry: true
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(
990
+ {
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 }
998
+ );
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
1028
+ }
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);
1038
+ }
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
505
1072
  }
506
1073
  }
507
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
+ );
508
1080
  }
509
- );
510
- if (!releaseWithPopulatedActionEntries) {
511
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
512
- }
513
- if (releaseWithPopulatedActionEntries.releasedAt) {
514
- throw new errors.ValidationError("Release already published");
515
- }
516
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
517
- throw new errors.ValidationError("No entries to publish");
518
- }
519
- const actions = {};
520
- for (const action of releaseWithPopulatedActionEntries.actions) {
521
- const contentTypeUid = action.contentType;
522
- if (!actions[contentTypeUid]) {
523
- actions[contentTypeUid] = {
524
- publish: [],
525
- unpublish: []
526
- };
527
- }
528
- if (action.type === "publish") {
529
- actions[contentTypeUid].publish.push(action.entry);
530
- } else {
531
- actions[contentTypeUid].unpublish.push(action.entry);
532
- }
533
- }
534
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
535
- await strapi2.db.transaction(async () => {
536
- for (const contentTypeUid of Object.keys(actions)) {
537
- const { publish, unpublish } = actions[contentTypeUid];
538
- if (publish.length > 0) {
539
- await entityManagerService.publishMany(publish, contentTypeUid);
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
540
1104
  }
541
- if (unpublish.length > 0) {
542
- await entityManagerService.unpublishMany(unpublish, contentTypeUid);
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
+ }
1118
+ }
543
1119
  }
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
+ );
544
1125
  }
545
- });
546
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
547
- data: {
548
- /*
549
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
550
- */
551
- // @ts-expect-error see above
552
- releasedAt: /* @__PURE__ */ new Date()
553
- }
554
- });
555
- return release2;
556
- },
557
- async updateAction(actionId, releaseId, update) {
558
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
559
- where: {
560
- id: actionId,
561
- release: {
562
- id: releaseId,
563
- releasedAt: {
564
- $null: true
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
+ }
565
1141
  }
566
- }
567
- },
568
- data: update
569
- });
570
- if (!updatedAction) {
571
- throw new errors.NotFoundError(
572
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
573
- );
574
- }
575
- return updatedAction;
576
- },
577
- async deleteAction(actionId, releaseId) {
578
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
579
- where: {
580
- id: actionId,
581
- release: {
582
- id: releaseId,
583
- releasedAt: {
584
- $null: true
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
585
1161
  }
1162
+ });
1163
+ if (!releasesUpdated.includes(action.release.id)) {
1164
+ releasesUpdated.push(action.release.id);
586
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
+ });
587
1175
  }
588
- });
589
- if (!deletedAction) {
590
- throw new errors.NotFoundError(
591
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
592
- );
593
1176
  }
594
- return deletedAction;
1177
+ };
1178
+ };
1179
+ class AlreadyOnReleaseError extends errors.ApplicationError {
1180
+ constructor(message) {
1181
+ super(message);
1182
+ this.name = "AlreadyOnReleaseError";
595
1183
  }
596
- });
1184
+ }
597
1185
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
598
1186
  async validateUniqueEntry(releaseId, releaseActionArgs) {
599
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
600
- 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
+ }
601
1194
  });
602
1195
  if (!release2) {
603
1196
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
604
1197
  }
605
1198
  const isEntryInRelease = release2.actions.some(
606
- (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)
607
1200
  );
608
1201
  if (isEntryInRelease) {
609
- throw new errors.ValidationError(
610
- `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}`
611
1204
  );
612
1205
  }
613
1206
  },
614
- validateEntryContentType(contentTypeUid) {
1207
+ validateEntryData(contentTypeUid, entryDocumentId) {
615
1208
  const contentType = strapi2.contentType(contentTypeUid);
616
1209
  if (!contentType) {
617
1210
  throw new errors.NotFoundError(`No content type found for uid ${contentTypeUid}`);
618
1211
  }
619
- if (!contentType.options?.draftAndPublish) {
1212
+ if (!contentTypes$1.hasDraftAndPublish(contentType)) {
620
1213
  throw new errors.ValidationError(
621
1214
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
622
1215
  );
623
1216
  }
1217
+ if (contentType.kind === "collectionType" && !entryDocumentId) {
1218
+ throw new errors.ValidationError("Document id is required for collection type");
1219
+ }
624
1220
  },
625
1221
  async validatePendingReleasesLimit() {
626
- const maximumPendingReleases = (
627
- // @ts-expect-error - options is not typed into features
628
- EE.features.get("cms-content-releases")?.options?.maximumReleases || 3
629
- );
1222
+ const featureCfg = strapi2.ee.features.get("cms-content-releases");
1223
+ const maximumPendingReleases = typeof featureCfg === "object" && featureCfg?.options?.maximumReleases || 3;
630
1224
  const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
631
1225
  filters: {
632
1226
  releasedAt: {
@@ -637,23 +1231,98 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
637
1231
  if (pendingReleasesCount >= maximumPendingReleases) {
638
1232
  throw new errors.ValidationError("You have reached the maximum number of pending releases");
639
1233
  }
1234
+ },
1235
+ async validateUniqueNameForPendingRelease(name, id) {
1236
+ const pendingReleases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1237
+ where: {
1238
+ releasedAt: {
1239
+ $null: true
1240
+ },
1241
+ name,
1242
+ ...id && { id: { $ne: id } }
1243
+ }
1244
+ });
1245
+ const isNameUnique = pendingReleases.length === 0;
1246
+ if (!isNameUnique) {
1247
+ throw new errors.ValidationError(`Release with name ${name} already exists`);
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
+ }
640
1254
  }
641
1255
  });
642
- const createEventManagerService = () => {
643
- const state = {
644
- destroyListenerCallbacks: []
645
- };
1256
+ const createSchedulingService = ({ strapi: strapi2 }) => {
1257
+ const scheduledJobs = /* @__PURE__ */ new Map();
646
1258
  return {
647
- addDestroyListenerCallback(destroyListenerCallback) {
648
- state.destroyListenerCallbacks.push(destroyListenerCallback);
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;
649
1276
  },
650
- destroyAllListeners() {
651
- if (!state.destroyListenerCallbacks.length) {
652
- return;
1277
+ cancel(releaseId) {
1278
+ if (scheduledJobs.has(releaseId)) {
1279
+ scheduledJobs.get(releaseId).cancel();
1280
+ scheduledJobs.delete(releaseId);
653
1281
  }
654
- state.destroyListenerCallbacks.forEach((destroyListenerCallback) => {
655
- destroyListenerCallback();
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
+ }
656
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
+ };
657
1326
  }
658
1327
  };
659
1328
  };
@@ -661,64 +1330,148 @@ const services = {
661
1330
  release: createReleaseService,
662
1331
  "release-action": createReleaseActionService,
663
1332
  "release-validation": createReleaseValidationService,
664
- "event-manager": createEventManagerService
1333
+ scheduling: createSchedulingService,
1334
+ settings: createSettingsService
665
1335
  };
666
- const RELEASE_SCHEMA = yup.object().shape({
667
- 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()
668
1350
  }).required().noUnknown();
669
1351
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
1352
+ const validatefindByDocumentAttachedParams = validateYupSchema(
1353
+ FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA
1354
+ );
670
1355
  const releaseController = {
671
- async findMany(ctx) {
672
- 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({
673
1363
  ability: ctx.state.userAbility,
674
1364
  model: RELEASE_MODEL_UID
675
1365
  });
676
1366
  await permissionsManager.validateQuery(ctx.query);
677
1367
  const releaseService = getService("release", { strapi });
678
- const isFindManyForContentTypeEntry = Boolean(ctx.query?.contentTypeUid && ctx.query?.entryId);
679
- if (isFindManyForContentTypeEntry) {
680
- const query = await permissionsManager.sanitizeQuery(ctx.query);
681
- const contentTypeUid = query.contentTypeUid;
682
- const entryId = query.entryId;
683
- const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
684
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
685
- hasEntryAttached
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: {
1391
+ actions: {
1392
+ fields: ["type"],
1393
+ filters: {
1394
+ contentType,
1395
+ entryDocumentId: entryDocumentId ?? null,
1396
+ locale: locale ?? null
1397
+ }
1398
+ }
1399
+ }
686
1400
  });
687
- ctx.body = { data };
1401
+ ctx.body = { data: releases };
688
1402
  } else {
689
- const query = await permissionsManager.sanitizeQuery(ctx.query);
690
- const { results, pagination } = await releaseService.findPage(query);
691
- const data = results.map((release2) => {
692
- const { actions, ...releaseData } = release2;
693
- return {
694
- ...releaseData,
1403
+ const relatedReleases = await releaseService.findMany({
1404
+ where: {
1405
+ releasedAt: null,
695
1406
  actions: {
696
- meta: {
697
- count: actions.count
698
- }
1407
+ contentType,
1408
+ entryDocumentId: entryDocumentId ?? null,
1409
+ locale: locale ?? null
699
1410
  }
700
- };
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
+ }
701
1427
  });
702
- ctx.body = { data, meta: { pagination } };
1428
+ ctx.body = { data: releases };
703
1429
  }
704
1430
  },
705
- async findOne(ctx) {
706
- const id = ctx.params.id;
707
- const releaseService = getService("release", { strapi });
708
- const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
709
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1431
+ async findPage(ctx) {
1432
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
710
1433
  ability: ctx.state.userAbility,
711
1434
  model: RELEASE_MODEL_UID
712
1435
  });
713
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
714
- const count = await releaseService.countActions({
715
- filters: {
716
- 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
717
1454
  }
718
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"] });
719
1463
  if (!release2) {
720
1464
  throw new errors.NotFoundError(`Release not found for id: ${id}`);
721
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
+ };
722
1475
  const data = {
723
1476
  ...sanitizedRelease,
724
1477
  actions: {
@@ -729,19 +1482,63 @@ const releaseController = {
729
1482
  };
730
1483
  ctx.body = { data };
731
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
+ },
732
1529
  async create(ctx) {
733
1530
  const user = ctx.state.user;
734
1531
  const releaseArgs = ctx.request.body;
735
1532
  await validateRelease(releaseArgs);
736
1533
  const releaseService = getService("release", { strapi });
737
1534
  const release2 = await releaseService.create(releaseArgs, { user });
738
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1535
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
739
1536
  ability: ctx.state.userAbility,
740
1537
  model: RELEASE_MODEL_UID
741
1538
  });
742
- ctx.body = {
1539
+ ctx.created({
743
1540
  data: await permissionsManager.sanitizeOutput(release2)
744
- };
1541
+ });
745
1542
  },
746
1543
  async update(ctx) {
747
1544
  const user = ctx.state.user;
@@ -750,7 +1547,7 @@ const releaseController = {
750
1547
  await validateRelease(releaseArgs);
751
1548
  const releaseService = getService("release", { strapi });
752
1549
  const release2 = await releaseService.update(id, releaseArgs, { user });
753
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1550
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
754
1551
  ability: ctx.state.userAbility,
755
1552
  model: RELEASE_MODEL_UID
756
1553
  });
@@ -767,67 +1564,135 @@ const releaseController = {
767
1564
  };
768
1565
  },
769
1566
  async publish(ctx) {
770
- const user = ctx.state.user;
771
1567
  const id = ctx.params.id;
772
1568
  const releaseService = getService("release", { strapi });
773
- 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
+ ]);
774
1585
  ctx.body = {
775
- data: release2
1586
+ data: release2,
1587
+ meta: {
1588
+ totalEntries: countPublishActions + countUnpublishActions,
1589
+ totalPublishedEntries: countPublishActions,
1590
+ totalUnpublishedEntries: countUnpublishActions
1591
+ }
776
1592
  };
777
1593
  }
778
1594
  };
779
1595
  const RELEASE_ACTION_SCHEMA = yup$1.object().shape({
780
- entry: yup$1.object().shape({
781
- id: yup$1.strapiID().required(),
782
- contentType: yup$1.string().required()
783
- }).required(),
1596
+ contentType: yup$1.string().required(),
1597
+ entryDocumentId: yup$1.strapiID(),
1598
+ locale: yup$1.string(),
784
1599
  type: yup$1.string().oneOf(["publish", "unpublish"]).required()
785
1600
  });
786
1601
  const RELEASE_ACTION_UPDATE_SCHEMA = yup$1.object().shape({
787
1602
  type: yup$1.string().oneOf(["publish", "unpublish"]).required()
788
1603
  });
1604
+ const FIND_MANY_ACTIONS_PARAMS = yup$1.object().shape({
1605
+ groupBy: yup$1.string().oneOf(["action", "contentType", "locale"])
1606
+ });
789
1607
  const validateReleaseAction = validateYupSchema(RELEASE_ACTION_SCHEMA);
790
1608
  const validateReleaseActionUpdateSchema = validateYupSchema(RELEASE_ACTION_UPDATE_SCHEMA);
1609
+ const validateFindManyActionsParams = validateYupSchema(FIND_MANY_ACTIONS_PARAMS);
791
1610
  const releaseActionController = {
792
1611
  async create(ctx) {
793
1612
  const releaseId = ctx.params.releaseId;
794
1613
  const releaseActionArgs = ctx.request.body;
795
1614
  await validateReleaseAction(releaseActionArgs);
796
- const releaseService = getService("release", { strapi });
797
- const releaseAction2 = await releaseService.createAction(releaseId, releaseActionArgs);
798
- ctx.body = {
1615
+ const releaseActionService = getService("release-action", { strapi });
1616
+ const releaseAction2 = await releaseActionService.create(releaseId, releaseActionArgs);
1617
+ ctx.created({
799
1618
  data: releaseAction2
800
- };
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
+ });
801
1658
  },
802
1659
  async findMany(ctx) {
803
1660
  const releaseId = ctx.params.releaseId;
804
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1661
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
805
1662
  ability: ctx.state.userAbility,
806
1663
  model: RELEASE_ACTION_MODEL_UID
807
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;
808
1673
  const query = await permissionsManager.sanitizeQuery(ctx.query);
809
- const releaseService = getService("release", { strapi });
810
- const { results, pagination } = await releaseService.findActions(releaseId, {
811
- sort: query.groupBy === "action" ? "type" : query.groupBy,
1674
+ const releaseActionService = getService("release-action", { strapi });
1675
+ const { results, pagination } = await releaseActionService.findPage(releaseId, {
812
1676
  ...query
813
1677
  });
814
1678
  const contentTypeOutputSanitizers = results.reduce((acc, action) => {
815
1679
  if (acc[action.contentType]) {
816
1680
  return acc;
817
1681
  }
818
- const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
1682
+ const contentTypePermissionsManager = strapi.service("admin::permission").createPermissionsManager({
819
1683
  ability: ctx.state.userAbility,
820
1684
  model: action.contentType
821
1685
  });
822
1686
  acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
823
1687
  return acc;
824
1688
  }, {});
825
- const sanitizedResults = await mapAsync(results, async (action) => ({
1689
+ const sanitizedResults = await async.map(results, async (action) => ({
826
1690
  ...action,
827
- entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1691
+ entry: action.entry ? await contentTypeOutputSanitizers[action.contentType](action.entry) : {}
828
1692
  }));
829
- const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
830
- 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 });
831
1696
  const components = await releaseService.getAllComponents();
832
1697
  ctx.body = {
833
1698
  data: groupedData,
@@ -843,8 +1708,8 @@ const releaseActionController = {
843
1708
  const releaseId = ctx.params.releaseId;
844
1709
  const releaseActionUpdateArgs = ctx.request.body;
845
1710
  await validateReleaseActionUpdateSchema(releaseActionUpdateArgs);
846
- const releaseService = getService("release", { strapi });
847
- const updatedAction = await releaseService.updateAction(
1711
+ const releaseActionService = getService("release-action", { strapi });
1712
+ const updatedAction = await releaseActionService.update(
848
1713
  actionId,
849
1714
  releaseId,
850
1715
  releaseActionUpdateArgs
@@ -856,17 +1721,71 @@ const releaseActionController = {
856
1721
  async delete(ctx) {
857
1722
  const actionId = ctx.params.actionId;
858
1723
  const releaseId = ctx.params.releaseId;
859
- const releaseService = getService("release", { strapi });
860
- const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
1724
+ const releaseActionService = getService("release-action", { strapi });
1725
+ const deletedReleaseAction = await releaseActionService.delete(actionId, releaseId);
861
1726
  ctx.body = {
862
1727
  data: deletedReleaseAction
863
1728
  };
864
1729
  }
865
1730
  };
866
- 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
+ };
867
1754
  const release = {
868
1755
  type: "admin",
869
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
+ },
870
1789
  {
871
1790
  method: "POST",
872
1791
  path: "/",
@@ -886,7 +1805,7 @@ const release = {
886
1805
  {
887
1806
  method: "GET",
888
1807
  path: "/",
889
- handler: "release.findMany",
1808
+ handler: "release.findPage",
890
1809
  config: {
891
1810
  policies: [
892
1811
  "admin::isAuthenticatedAdmin",
@@ -984,6 +1903,22 @@ const releaseAction = {
984
1903
  ]
985
1904
  }
986
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
+ },
987
1922
  {
988
1923
  method: "GET",
989
1924
  path: "/:releaseId/actions",
@@ -1034,28 +1969,64 @@ const releaseAction = {
1034
1969
  }
1035
1970
  ]
1036
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
+ };
1037
2009
  const routes = {
2010
+ settings,
1038
2011
  release,
1039
2012
  "release-action": releaseAction
1040
2013
  };
1041
- const { features } = require("@strapi/strapi/dist/utils/ee");
1042
2014
  const getPlugin = () => {
1043
- if (features.isEnabled("cms-content-releases")) {
2015
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
1044
2016
  return {
1045
2017
  register,
1046
2018
  bootstrap,
2019
+ destroy,
1047
2020
  contentTypes,
1048
2021
  services,
1049
2022
  controllers,
1050
- routes,
1051
- destroy() {
1052
- if (features.isEnabled("cms-content-releases")) {
1053
- getService("event-manager").destroyAllListeners();
1054
- }
1055
- }
2023
+ routes
1056
2024
  };
1057
2025
  }
1058
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
1059
2030
  contentTypes
1060
2031
  };
1061
2032
  };