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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/LICENSE +17 -1
  2. package/dist/_chunks/App-CiZCkScI.mjs +1558 -0
  3. package/dist/_chunks/App-CiZCkScI.mjs.map +1 -0
  4. package/dist/_chunks/App-SGjO5UPV.js +1578 -0
  5. package/dist/_chunks/App-SGjO5UPV.js.map +1 -0
  6. package/dist/_chunks/{PurchaseContentReleases-YhAPgpG9.js → PurchaseContentReleases--qQepXpP.js} +9 -8
  7. package/dist/_chunks/PurchaseContentReleases--qQepXpP.js.map +1 -0
  8. package/dist/_chunks/{PurchaseContentReleases-Clm0iACO.mjs → PurchaseContentReleases-D-n-w-st.mjs} +10 -9
  9. package/dist/_chunks/PurchaseContentReleases-D-n-w-st.mjs.map +1 -0
  10. package/dist/_chunks/ReleasesSettingsPage-Cto_NLUd.js +178 -0
  11. package/dist/_chunks/ReleasesSettingsPage-Cto_NLUd.js.map +1 -0
  12. package/dist/_chunks/ReleasesSettingsPage-DQT8N3A-.mjs +178 -0
  13. package/dist/_chunks/ReleasesSettingsPage-DQT8N3A-.mjs.map +1 -0
  14. package/dist/_chunks/{en-r0otWaln.js → en-BWPPsSH-.js} +31 -6
  15. package/dist/_chunks/en-BWPPsSH-.js.map +1 -0
  16. package/dist/_chunks/{en-veqvqeEr.mjs → en-D9Q4YW03.mjs} +31 -6
  17. package/dist/_chunks/en-D9Q4YW03.mjs.map +1 -0
  18. package/dist/_chunks/index-BjvFfTtA.mjs +1386 -0
  19. package/dist/_chunks/index-BjvFfTtA.mjs.map +1 -0
  20. package/dist/_chunks/index-CyU534vL.js +1404 -0
  21. package/dist/_chunks/index-CyU534vL.js.map +1 -0
  22. package/dist/_chunks/schemas-DBYv9gK8.js +61 -0
  23. package/dist/_chunks/schemas-DBYv9gK8.js.map +1 -0
  24. package/dist/_chunks/schemas-DdA2ic2U.mjs +44 -0
  25. package/dist/_chunks/schemas-DdA2ic2U.mjs.map +1 -0
  26. package/dist/admin/index.js +1 -15
  27. package/dist/admin/index.js.map +1 -1
  28. package/dist/admin/index.mjs +2 -16
  29. package/dist/admin/index.mjs.map +1 -1
  30. package/dist/admin/src/components/EntryValidationPopover.d.ts +13 -0
  31. package/dist/admin/src/components/RelativeTime.d.ts +28 -0
  32. package/dist/admin/src/components/ReleaseAction.d.ts +3 -0
  33. package/dist/admin/src/components/ReleaseActionMenu.d.ts +26 -0
  34. package/dist/admin/src/components/ReleaseActionModal.d.ts +24 -0
  35. package/dist/admin/src/components/ReleaseActionOptions.d.ts +9 -0
  36. package/dist/admin/src/components/ReleaseListCell.d.ts +28 -0
  37. package/dist/admin/src/components/ReleaseModal.d.ts +17 -0
  38. package/dist/admin/src/components/ReleasesPanel.d.ts +3 -0
  39. package/dist/admin/src/constants.d.ts +76 -0
  40. package/dist/admin/src/index.d.ts +3 -0
  41. package/dist/admin/src/modules/hooks.d.ts +7 -0
  42. package/dist/admin/src/pages/App.d.ts +1 -0
  43. package/dist/admin/src/pages/PurchaseContentReleases.d.ts +2 -0
  44. package/dist/admin/src/pages/ReleaseDetailsPage.d.ts +2 -0
  45. package/dist/admin/src/pages/ReleasesPage.d.ts +8 -0
  46. package/dist/admin/src/pages/ReleasesSettingsPage.d.ts +1 -0
  47. package/dist/admin/src/pages/tests/mockReleaseDetailsPageData.d.ts +181 -0
  48. package/dist/admin/src/pages/tests/mockReleasesPageData.d.ts +39 -0
  49. package/dist/admin/src/pluginId.d.ts +1 -0
  50. package/dist/admin/src/services/release.d.ts +112 -0
  51. package/dist/admin/src/store/hooks.d.ts +7 -0
  52. package/dist/admin/src/utils/api.d.ts +6 -0
  53. package/dist/admin/src/utils/prefixPluginTranslations.d.ts +3 -0
  54. package/dist/admin/src/utils/time.d.ts +10 -0
  55. package/dist/admin/src/validation/schemas.d.ts +6 -0
  56. package/dist/server/index.js +1233 -515
  57. package/dist/server/index.js.map +1 -1
  58. package/dist/server/index.mjs +1232 -513
  59. package/dist/server/index.mjs.map +1 -1
  60. package/dist/server/src/bootstrap.d.ts +5 -0
  61. package/dist/server/src/bootstrap.d.ts.map +1 -0
  62. package/dist/server/src/constants.d.ts +21 -0
  63. package/dist/server/src/constants.d.ts.map +1 -0
  64. package/dist/server/src/content-types/index.d.ts +97 -0
  65. package/dist/server/src/content-types/index.d.ts.map +1 -0
  66. package/dist/server/src/content-types/release/index.d.ts +48 -0
  67. package/dist/server/src/content-types/release/index.d.ts.map +1 -0
  68. package/dist/server/src/content-types/release/schema.d.ts +47 -0
  69. package/dist/server/src/content-types/release/schema.d.ts.map +1 -0
  70. package/dist/server/src/content-types/release-action/index.d.ts +48 -0
  71. package/dist/server/src/content-types/release-action/index.d.ts.map +1 -0
  72. package/dist/server/src/content-types/release-action/schema.d.ts +47 -0
  73. package/dist/server/src/content-types/release-action/schema.d.ts.map +1 -0
  74. package/dist/server/src/controllers/index.d.ts +25 -0
  75. package/dist/server/src/controllers/index.d.ts.map +1 -0
  76. package/dist/server/src/controllers/release-action.d.ts +10 -0
  77. package/dist/server/src/controllers/release-action.d.ts.map +1 -0
  78. package/dist/server/src/controllers/release.d.ts +18 -0
  79. package/dist/server/src/controllers/release.d.ts.map +1 -0
  80. package/dist/server/src/controllers/settings.d.ts +11 -0
  81. package/dist/server/src/controllers/settings.d.ts.map +1 -0
  82. package/dist/server/src/controllers/validation/release-action.d.ts +14 -0
  83. package/dist/server/src/controllers/validation/release-action.d.ts.map +1 -0
  84. package/dist/server/src/controllers/validation/release.d.ts +4 -0
  85. package/dist/server/src/controllers/validation/release.d.ts.map +1 -0
  86. package/dist/server/src/controllers/validation/settings.d.ts +3 -0
  87. package/dist/server/src/controllers/validation/settings.d.ts.map +1 -0
  88. package/dist/server/src/destroy.d.ts +5 -0
  89. package/dist/server/src/destroy.d.ts.map +1 -0
  90. package/dist/server/src/index.d.ts +2111 -0
  91. package/dist/server/src/index.d.ts.map +1 -0
  92. package/dist/server/src/middlewares/documents.d.ts +6 -0
  93. package/dist/server/src/middlewares/documents.d.ts.map +1 -0
  94. package/dist/server/src/migrations/database/5.0.0-document-id-in-actions.d.ts +9 -0
  95. package/dist/server/src/migrations/database/5.0.0-document-id-in-actions.d.ts.map +1 -0
  96. package/dist/server/src/migrations/index.d.ts +13 -0
  97. package/dist/server/src/migrations/index.d.ts.map +1 -0
  98. package/dist/server/src/register.d.ts +5 -0
  99. package/dist/server/src/register.d.ts.map +1 -0
  100. package/dist/server/src/routes/index.d.ts +51 -0
  101. package/dist/server/src/routes/index.d.ts.map +1 -0
  102. package/dist/server/src/routes/release-action.d.ts +18 -0
  103. package/dist/server/src/routes/release-action.d.ts.map +1 -0
  104. package/dist/server/src/routes/release.d.ts +18 -0
  105. package/dist/server/src/routes/release.d.ts.map +1 -0
  106. package/dist/server/src/routes/settings.d.ts +18 -0
  107. package/dist/server/src/routes/settings.d.ts.map +1 -0
  108. package/dist/server/src/services/index.d.ts +1824 -0
  109. package/dist/server/src/services/index.d.ts.map +1 -0
  110. package/dist/server/src/services/release-action.d.ts +34 -0
  111. package/dist/server/src/services/release-action.d.ts.map +1 -0
  112. package/dist/server/src/services/release.d.ts +31 -0
  113. package/dist/server/src/services/release.d.ts.map +1 -0
  114. package/dist/server/src/services/scheduling.d.ts +18 -0
  115. package/dist/server/src/services/scheduling.d.ts.map +1 -0
  116. package/dist/server/src/services/settings.d.ts +13 -0
  117. package/dist/server/src/services/settings.d.ts.map +1 -0
  118. package/dist/server/src/services/validation.d.ts +18 -0
  119. package/dist/server/src/services/validation.d.ts.map +1 -0
  120. package/dist/server/src/utils/index.d.ts +35 -0
  121. package/dist/server/src/utils/index.d.ts.map +1 -0
  122. package/dist/shared/contracts/release-actions.d.ts +137 -0
  123. package/dist/shared/contracts/release-actions.d.ts.map +1 -0
  124. package/dist/shared/contracts/releases.d.ts +184 -0
  125. package/dist/shared/contracts/releases.d.ts.map +1 -0
  126. package/dist/shared/contracts/settings.d.ts +39 -0
  127. package/dist/shared/contracts/settings.d.ts.map +1 -0
  128. package/dist/shared/types.d.ts +24 -0
  129. package/dist/shared/types.d.ts.map +1 -0
  130. package/package.json +35 -40
  131. package/dist/_chunks/App-OK4Xac-O.js +0 -1315
  132. package/dist/_chunks/App-OK4Xac-O.js.map +0 -1
  133. package/dist/_chunks/App-xAkiD42p.mjs +0 -1292
  134. package/dist/_chunks/App-xAkiD42p.mjs.map +0 -1
  135. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs.map +0 -1
  136. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js.map +0 -1
  137. package/dist/_chunks/en-r0otWaln.js.map +0 -1
  138. package/dist/_chunks/en-veqvqeEr.mjs.map +0 -1
  139. package/dist/_chunks/index-JvA2_26n.js +0 -1015
  140. package/dist/_chunks/index-JvA2_26n.js.map +0 -1
  141. package/dist/_chunks/index-exoiSU3V.mjs +0 -994
  142. package/dist/_chunks/index-exoiSU3V.mjs.map +0 -1
  143. package/strapi-server.js +0 -3
@@ -1,7 +1,7 @@
1
- import { contentTypes as contentTypes$1, mapAsync, setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
1
+ import { contentTypes as contentTypes$1, async, setCreatorFields, errors, yup as yup$1, validateYupSchema } from "@strapi/utils";
2
+ 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
5
  import { scheduleJob } from "node-schedule";
6
6
  import * as yup from "yup";
7
7
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
@@ -48,11 +48,94 @@ const ACTIONS = [
48
48
  displayName: "Add an entry to a release",
49
49
  uid: "create-action",
50
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"
51
68
  }
52
69
  ];
53
70
  const ALLOWED_WEBHOOK_EVENTS = {
54
71
  RELEASES_PUBLISH: "releases.publish"
55
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
+ };
56
139
  async function deleteActionsOnDisableDraftAndPublish({
57
140
  oldContentTypes,
58
141
  contentTypes: contentTypes2
@@ -74,90 +157,354 @@ async function deleteActionsOnDisableDraftAndPublish({
74
157
  async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
75
158
  const deletedContentTypes = difference(keys(oldContentTypes), keys(contentTypes2)) ?? [];
76
159
  if (deletedContentTypes.length) {
77
- await mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
160
+ await async.map(deletedContentTypes, async (deletedContentTypeUID) => {
78
161
  return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
79
162
  });
80
163
  }
81
164
  }
82
- 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
+ };
83
349
  const register = async ({ strapi: strapi2 }) => {
84
- if (features$2.isEnabled("cms-content-releases")) {
85
- await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
86
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
87
- 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();
88
360
  }
89
361
  };
90
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
91
- return strapi2.plugin("content-releases").service(name);
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);
459
+ }
92
460
  };
93
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
94
461
  const bootstrap = async ({ strapi: strapi2 }) => {
95
- 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
+ );
96
466
  strapi2.db.lifecycles.subscribe({
97
- afterDelete(event) {
98
- const { model, result } = event;
99
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
100
- const { id } = result;
101
- strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
102
- where: {
103
- target_type: model.uid,
104
- target_id: id
105
- }
106
- });
107
- }
108
- },
109
- /**
110
- * deleteMany hook doesn't return the deleted entries ids
111
- * so we need to fetch them before deleting the entries to save the ids on our state
112
- */
113
- async beforeDeleteMany(event) {
114
- const { model, params } = event;
115
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
116
- const { where } = params;
117
- const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
118
- event.state.entriesToDelete = entriesToDelete;
119
- }
120
- },
467
+ models: contentTypesWithDraftAndPublish,
121
468
  /**
122
- * We delete the release actions related to deleted entries
123
- * 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
124
470
  */
125
471
  async afterDeleteMany(event) {
126
- const { model, state } = event;
127
- const entriesToDelete = state.entriesToDelete;
128
- if (entriesToDelete) {
129
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
130
- where: {
131
- target_type: model.uid,
132
- target_id: {
133
- $in: entriesToDelete.map((entry) => entry.id)
134
- }
135
- }
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
136
485
  });
137
486
  }
138
487
  }
139
488
  });
140
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
141
- getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
142
- strapi2.log.error(
143
- "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
144
- );
145
- throw err;
146
- });
147
- Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
148
- strapi2.webhookStore.addAllowedEvent(key, value);
149
- });
150
- }
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
+ });
151
500
  }
152
501
  };
153
502
  const destroy = async ({ strapi: strapi2 }) => {
154
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
155
- const scheduledJobs = getService("scheduling", {
156
- strapi: strapi2
157
- }).getAll();
158
- for (const [, job] of scheduledJobs) {
159
- job.cancel();
160
- }
503
+ const scheduledJobs = getService("scheduling", {
504
+ strapi: strapi2
505
+ }).getAll();
506
+ for (const [, job] of scheduledJobs) {
507
+ job.cancel();
161
508
  }
162
509
  };
163
510
  const schema$1 = {
@@ -192,6 +539,11 @@ const schema$1 = {
192
539
  timezone: {
193
540
  type: "string"
194
541
  },
542
+ status: {
543
+ type: "enumeration",
544
+ enum: ["ready", "blocked", "failed", "done", "empty"],
545
+ required: true
546
+ },
195
547
  actions: {
196
548
  type: "relation",
197
549
  relation: "oneToMany",
@@ -227,15 +579,13 @@ const schema = {
227
579
  enum: ["publish", "unpublish"],
228
580
  required: true
229
581
  },
230
- entry: {
231
- type: "relation",
232
- relation: "morphToOne",
233
- configurable: false
234
- },
235
582
  contentType: {
236
583
  type: "string",
237
584
  required: true
238
585
  },
586
+ entryDocumentId: {
587
+ type: "string"
588
+ },
239
589
  locale: {
240
590
  type: "string"
241
591
  },
@@ -244,6 +594,9 @@ const schema = {
244
594
  relation: "manyToOne",
245
595
  target: RELEASE_MODEL_UID,
246
596
  inversedBy: "actions"
597
+ },
598
+ isEntryValid: {
599
+ type: "boolean"
247
600
  }
248
601
  }
249
602
  };
@@ -254,18 +607,6 @@ const contentTypes = {
254
607
  release: release$1,
255
608
  "release-action": releaseAction$1
256
609
  };
257
- const getGroupName = (queryValue) => {
258
- switch (queryValue) {
259
- case "contentType":
260
- return "contentType.displayName";
261
- case "action":
262
- return "type";
263
- case "locale":
264
- return _.getOr("No locale", "locale.name");
265
- default:
266
- return "contentType.displayName";
267
- }
268
- };
269
610
  const createReleaseService = ({ strapi: strapi2 }) => {
270
611
  const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
271
612
  strapi2.eventHub.emit(event, {
@@ -274,6 +615,33 @@ const createReleaseService = ({ strapi: strapi2 }) => {
274
615
  release: release2
275
616
  });
276
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
623
+ }
624
+ }
625
+ });
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
+ };
277
645
  return {
278
646
  async create(releaseData, { user }) {
279
647
  const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
@@ -287,10 +655,13 @@ const createReleaseService = ({ strapi: strapi2 }) => {
287
655
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
288
656
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
289
657
  ]);
290
- const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
291
- data: releaseWithCreatorFields
658
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).create({
659
+ data: {
660
+ ...releaseWithCreatorFields,
661
+ status: "empty"
662
+ }
292
663
  });
293
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
664
+ if (releaseWithCreatorFields.scheduledAt) {
294
665
  const schedulingService = getService("scheduling", { strapi: strapi2 });
295
666
  await schedulingService.set(release2.id, release2.scheduledAt);
296
667
  }
@@ -298,94 +669,28 @@ const createReleaseService = ({ strapi: strapi2 }) => {
298
669
  return release2;
299
670
  },
300
671
  async findOne(id, query = {}) {
301
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
302
- ...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 }
303
676
  });
304
677
  return release2;
305
678
  },
306
679
  findPage(query) {
307
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
308
- ...query,
680
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
681
+ return strapi2.db.query(RELEASE_MODEL_UID).findPage({
682
+ ...dbQuery,
309
683
  populate: {
310
684
  actions: {
311
- // @ts-expect-error Ignore missing properties
312
685
  count: true
313
686
  }
314
687
  }
315
688
  });
316
689
  },
317
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
318
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
319
- where: {
320
- actions: {
321
- target_type: contentTypeUid,
322
- target_id: entryId
323
- },
324
- releasedAt: {
325
- $null: true
326
- }
327
- },
328
- populate: {
329
- // Filter the action to get only the content type entry
330
- actions: {
331
- where: {
332
- target_type: contentTypeUid,
333
- target_id: entryId
334
- }
335
- }
336
- }
337
- });
338
- return releases.map((release2) => {
339
- if (release2.actions?.length) {
340
- const [actionForEntry] = release2.actions;
341
- delete release2.actions;
342
- return {
343
- ...release2,
344
- action: actionForEntry
345
- };
346
- }
347
- return release2;
348
- });
349
- },
350
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
351
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
352
- where: {
353
- releasedAt: {
354
- $null: true
355
- },
356
- actions: {
357
- target_type: contentTypeUid,
358
- target_id: entryId
359
- }
360
- }
361
- });
362
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
363
- where: {
364
- $or: [
365
- {
366
- id: {
367
- $notIn: releasesRelated.map((release2) => release2.id)
368
- }
369
- },
370
- {
371
- actions: null
372
- }
373
- ],
374
- releasedAt: {
375
- $null: true
376
- }
377
- }
378
- });
379
- return releases.map((release2) => {
380
- if (release2.actions?.length) {
381
- const [actionForEntry] = release2.actions;
382
- delete release2.actions;
383
- return {
384
- ...release2,
385
- action: actionForEntry
386
- };
387
- }
388
- return release2;
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
389
694
  });
390
695
  },
391
696
  async update(id, releaseData, { user }) {
@@ -400,151 +705,27 @@ const createReleaseService = ({ strapi: strapi2 }) => {
400
705
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
401
706
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
402
707
  ]);
403
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
708
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id } });
404
709
  if (!release2) {
405
710
  throw new errors.NotFoundError(`No release found for id ${id}`);
406
711
  }
407
712
  if (release2.releasedAt) {
408
713
  throw new errors.ValidationError("Release already published");
409
714
  }
410
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
411
- /*
412
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
413
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
414
- */
415
- // @ts-expect-error see above
715
+ const updatedRelease = await strapi2.db.query(RELEASE_MODEL_UID).update({
716
+ where: { id },
416
717
  data: releaseWithCreatorFields
417
718
  });
418
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
419
- const schedulingService = getService("scheduling", { strapi: strapi2 });
420
- if (releaseData.scheduledAt) {
421
- await schedulingService.set(id, releaseData.scheduledAt);
422
- } else if (release2.scheduledAt) {
423
- schedulingService.cancel(id);
424
- }
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);
425
724
  }
725
+ this.updateReleaseStatus(id);
426
726
  strapi2.telemetry.send("didUpdateContentRelease");
427
727
  return updatedRelease;
428
728
  },
429
- async createAction(releaseId, action) {
430
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
431
- strapi: strapi2
432
- });
433
- await Promise.all([
434
- validateEntryContentType(action.entry.contentType),
435
- validateUniqueEntry(releaseId, action)
436
- ]);
437
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
438
- if (!release2) {
439
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
440
- }
441
- if (release2.releasedAt) {
442
- throw new errors.ValidationError("Release already published");
443
- }
444
- const { entry, type } = action;
445
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
446
- data: {
447
- type,
448
- contentType: entry.contentType,
449
- locale: entry.locale,
450
- entry: {
451
- id: entry.id,
452
- __type: entry.contentType,
453
- __pivot: { field: "entry" }
454
- },
455
- release: releaseId
456
- },
457
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
458
- });
459
- },
460
- async findActions(releaseId, query) {
461
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
462
- fields: ["id"]
463
- });
464
- if (!release2) {
465
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
466
- }
467
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
468
- ...query,
469
- populate: {
470
- entry: {
471
- populate: "*"
472
- }
473
- },
474
- filters: {
475
- release: releaseId
476
- }
477
- });
478
- },
479
- async countActions(query) {
480
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
481
- },
482
- async groupActions(actions, groupBy) {
483
- const contentTypeUids = actions.reduce((acc, action) => {
484
- if (!acc.includes(action.contentType)) {
485
- acc.push(action.contentType);
486
- }
487
- return acc;
488
- }, []);
489
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
490
- contentTypeUids
491
- );
492
- const allLocalesDictionary = await this.getLocalesDataForActions();
493
- const formattedData = actions.map((action) => {
494
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
495
- return {
496
- ...action,
497
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
498
- contentType: {
499
- displayName,
500
- mainFieldValue: action.entry[mainField],
501
- uid: action.contentType
502
- }
503
- };
504
- });
505
- const groupName = getGroupName(groupBy);
506
- return _.groupBy(groupName)(formattedData);
507
- },
508
- async getLocalesDataForActions() {
509
- if (!strapi2.plugin("i18n")) {
510
- return {};
511
- }
512
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
513
- return allLocales.reduce((acc, locale) => {
514
- acc[locale.code] = { name: locale.name, code: locale.code };
515
- return acc;
516
- }, {});
517
- },
518
- async getContentTypesDataForActions(contentTypesUids) {
519
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
520
- const contentTypesData = {};
521
- for (const contentTypeUid of contentTypesUids) {
522
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
523
- uid: contentTypeUid
524
- });
525
- contentTypesData[contentTypeUid] = {
526
- mainField: contentTypeConfig.settings.mainField,
527
- displayName: strapi2.getModel(contentTypeUid).info.displayName
528
- };
529
- }
530
- return contentTypesData;
531
- },
532
- getContentTypeModelsFromActions(actions) {
533
- const contentTypeUids = actions.reduce((acc, action) => {
534
- if (!acc.includes(action.contentType)) {
535
- acc.push(action.contentType);
536
- }
537
- return acc;
538
- }, []);
539
- const contentTypeModelsMap = contentTypeUids.reduce(
540
- (acc, contentTypeUid) => {
541
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
542
- return acc;
543
- },
544
- {}
545
- );
546
- return contentTypeModelsMap;
547
- },
548
729
  async getAllComponents() {
549
730
  const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
550
731
  const components = await contentManagerComponentsService.findAllComponents();
@@ -558,10 +739,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
558
739
  return componentsMap;
559
740
  },
560
741
  async delete(releaseId) {
561
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
742
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
743
+ where: { id: releaseId },
562
744
  populate: {
563
745
  actions: {
564
- fields: ["id"]
746
+ select: ["id"]
565
747
  }
566
748
  }
567
749
  });
@@ -579,9 +761,13 @@ const createReleaseService = ({ strapi: strapi2 }) => {
579
761
  }
580
762
  }
581
763
  });
582
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
764
+ await strapi2.db.query(RELEASE_MODEL_UID).delete({
765
+ where: {
766
+ id: releaseId
767
+ }
768
+ });
583
769
  });
584
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
770
+ if (release2.scheduledAt) {
585
771
  const schedulingService = getService("scheduling", { strapi: strapi2 });
586
772
  await schedulingService.cancel(release2.id);
587
773
  }
@@ -589,142 +775,294 @@ const createReleaseService = ({ strapi: strapi2 }) => {
589
775
  return release2;
590
776
  },
591
777
  async publish(releaseId) {
592
- try {
593
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
594
- RELEASE_MODEL_UID,
595
- releaseId,
596
- {
597
- populate: {
598
- actions: {
599
- populate: {
600
- entry: {
601
- fields: ["id"]
602
- }
603
- }
604
- }
605
- }
606
- }
607
- );
608
- if (!releaseWithPopulatedActionEntries) {
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) {
609
784
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
610
785
  }
611
- if (releaseWithPopulatedActionEntries.releasedAt) {
786
+ if (lockedRelease.releasedAt) {
612
787
  throw new errors.ValidationError("Release already published");
613
788
  }
614
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
615
- throw new errors.ValidationError("No entries to publish");
789
+ if (lockedRelease.status === "failed") {
790
+ throw new errors.ValidationError("Release failed to publish");
616
791
  }
617
- const collectionTypeActions = {};
618
- const singleTypeActions = [];
619
- for (const action of releaseWithPopulatedActionEntries.actions) {
620
- const contentTypeUid = action.contentType;
621
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
622
- if (!collectionTypeActions[contentTypeUid]) {
623
- collectionTypeActions[contentTypeUid] = {
624
- entriestoPublishIds: [],
625
- entriesToUnpublishIds: []
626
- };
627
- }
628
- if (action.type === "publish") {
629
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
630
- } else {
631
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
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()
632
814
  }
633
- } else {
634
- singleTypeActions.push({
635
- uid: contentTypeUid,
636
- action: action.type,
637
- id: action.entry.id
638
- });
639
- }
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
+ };
640
834
  }
641
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
642
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
643
- await strapi2.db.transaction(async () => {
644
- for (const { uid, action, id } of singleTypeActions) {
645
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
646
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
647
- try {
648
- if (action === "publish") {
649
- await entityManagerService.publish(entry, uid);
650
- } else {
651
- await entityManagerService.unpublish(entry, uid);
652
- }
653
- } catch (error) {
654
- if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft")) {
655
- } else {
656
- throw error;
657
- }
658
- }
835
+ });
836
+ if (error instanceof Error) {
837
+ throw error;
838
+ }
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
659
847
  }
660
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
661
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
662
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
663
- const entriesToPublish = await strapi2.entityService.findMany(
664
- contentTypeUid,
665
- {
666
- filters: {
667
- id: {
668
- $in: entriestoPublishIds
669
- }
670
- },
671
- populate
672
- }
673
- );
674
- const entriesToUnpublish = await strapi2.entityService.findMany(
675
- contentTypeUid,
676
- {
677
- filters: {
678
- id: {
679
- $in: entriesToUnpublishIds
680
- }
681
- },
682
- populate
683
- }
684
- );
685
- if (entriesToPublish.length > 0) {
686
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
687
- }
688
- if (entriesToUnpublish.length > 0) {
689
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
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"
690
864
  }
865
+ });
866
+ }
867
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
868
+ where: {
869
+ id: releaseId
870
+ },
871
+ data: {
872
+ status: "ready"
691
873
  }
692
874
  });
693
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
694
- data: {
695
- /*
696
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
697
- */
698
- // @ts-expect-error see above
699
- releasedAt: /* @__PURE__ */ new Date()
875
+ }
876
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
877
+ where: {
878
+ id: releaseId
879
+ },
880
+ data: {
881
+ status: "empty"
882
+ }
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 () => {
901
+ if (!strapi2.plugin("i18n")) {
902
+ return {};
903
+ }
904
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
905
+ return allLocales.reduce((acc, locale) => {
906
+ acc[locale.code] = { name: locale.name, code: locale.code };
907
+ return acc;
908
+ }, {});
909
+ };
910
+ const getContentTypesDataForActions = async (contentTypesUids) => {
911
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
912
+ const contentTypesData = {};
913
+ for (const contentTypeUid of contentTypesUids) {
914
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
915
+ uid: contentTypeUid
916
+ });
917
+ contentTypesData[contentTypeUid] = {
918
+ mainField: contentTypeConfig.settings.mainField,
919
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
920
+ };
921
+ }
922
+ return contentTypesData;
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;
940
+ }
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
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);
968
+ }
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,
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(
990
+ {
991
+ contentType: action.contentType,
992
+ documentId: action.entryDocumentId,
993
+ locale: action.locale,
994
+ populate,
995
+ status: action.type === "publish" ? "draft" : "published"
700
996
  },
701
- populate: {
702
- actions: {
703
- // @ts-expect-error is not expecting count but it is working
704
- count: true
705
- }
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
706
1028
  }
707
- });
708
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
709
- dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
710
- isPublished: true,
711
- release: release2
712
- });
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);
713
1038
  }
714
- strapi2.telemetry.send("didPublishContentRelease");
715
- return release2;
716
- } catch (error) {
717
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
718
- dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
719
- isPublished: false,
720
- error
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"
721
1048
  });
722
- }
723
- throw error;
724
- }
1049
+ acc[contentTypeUid] = {
1050
+ ...contentTypeModel,
1051
+ hasReviewWorkflow: !!workflow,
1052
+ stageRequiredToPublish: workflow?.stageRequiredToPublish
1053
+ };
1054
+ return acc;
1055
+ },
1056
+ {}
1057
+ );
1058
+ return contentTypeModelsMap;
725
1059
  },
726
- async updateAction(actionId, releaseId, update) {
727
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
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({
728
1066
  where: {
729
1067
  id: actionId,
730
1068
  release: {
@@ -733,17 +1071,42 @@ const createReleaseService = ({ strapi: strapi2 }) => {
733
1071
  $null: true
734
1072
  }
735
1073
  }
736
- },
737
- data: update
1074
+ }
738
1075
  });
739
- if (!updatedAction) {
1076
+ if (!action) {
740
1077
  throw new errors.NotFoundError(
741
1078
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
742
1079
  );
743
1080
  }
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);
744
1107
  return updatedAction;
745
1108
  },
746
- async deleteAction(actionId, releaseId) {
1109
+ async delete(actionId, releaseId) {
747
1110
  const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
748
1111
  where: {
749
1112
  id: actionId,
@@ -760,43 +1123,104 @@ const createReleaseService = ({ strapi: strapi2 }) => {
760
1123
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
761
1124
  );
762
1125
  }
1126
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
763
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
1161
+ }
1162
+ });
1163
+ if (!releasesUpdated.includes(action.release.id)) {
1164
+ releasesUpdated.push(action.release.id);
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
+ });
1175
+ }
764
1176
  }
765
1177
  };
766
1178
  };
1179
+ class AlreadyOnReleaseError extends errors.ApplicationError {
1180
+ constructor(message) {
1181
+ super(message);
1182
+ this.name = "AlreadyOnReleaseError";
1183
+ }
1184
+ }
767
1185
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
768
1186
  async validateUniqueEntry(releaseId, releaseActionArgs) {
769
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
770
- 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
+ }
771
1194
  });
772
1195
  if (!release2) {
773
1196
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
774
1197
  }
775
1198
  const isEntryInRelease = release2.actions.some(
776
- (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)
777
1200
  );
778
1201
  if (isEntryInRelease) {
779
- throw new errors.ValidationError(
780
- `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}`
781
1204
  );
782
1205
  }
783
1206
  },
784
- validateEntryContentType(contentTypeUid) {
1207
+ validateEntryData(contentTypeUid, entryDocumentId) {
785
1208
  const contentType = strapi2.contentType(contentTypeUid);
786
1209
  if (!contentType) {
787
1210
  throw new errors.NotFoundError(`No content type found for uid ${contentTypeUid}`);
788
1211
  }
789
- if (!contentType.options?.draftAndPublish) {
1212
+ if (!contentTypes$1.hasDraftAndPublish(contentType)) {
790
1213
  throw new errors.ValidationError(
791
1214
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
792
1215
  );
793
1216
  }
1217
+ if (contentType.kind === "collectionType" && !entryDocumentId) {
1218
+ throw new errors.ValidationError("Document id is required for collection type");
1219
+ }
794
1220
  },
795
1221
  async validatePendingReleasesLimit() {
796
- const maximumPendingReleases = (
797
- // @ts-expect-error - options is not typed into features
798
- EE.features.get("cms-content-releases")?.options?.maximumReleases || 3
799
- );
1222
+ const featureCfg = strapi2.ee.features.get("cms-content-releases");
1223
+ const maximumPendingReleases = typeof featureCfg === "object" && featureCfg?.options?.maximumReleases || 3;
800
1224
  const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
801
1225
  filters: {
802
1226
  releasedAt: {
@@ -809,8 +1233,8 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
809
1233
  }
810
1234
  },
811
1235
  async validateUniqueNameForPendingRelease(name, id) {
812
- const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
813
- filters: {
1236
+ const pendingReleases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1237
+ where: {
814
1238
  releasedAt: {
815
1239
  $null: true
816
1240
  },
@@ -839,7 +1263,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
839
1263
  }
840
1264
  const job = scheduleJob(scheduleDate, async () => {
841
1265
  try {
842
- await getService("release").publish(releaseId);
1266
+ await getService("release", { strapi: strapi2 }).publish(releaseId);
843
1267
  } catch (error) {
844
1268
  }
845
1269
  this.cancel(releaseId);
@@ -881,80 +1305,172 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
881
1305
  }
882
1306
  };
883
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
+ };
884
1329
  const services = {
885
1330
  release: createReleaseService,
1331
+ "release-action": createReleaseActionService,
886
1332
  "release-validation": createReleaseValidationService,
887
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1333
+ scheduling: createSchedulingService,
1334
+ settings: createSettingsService
888
1335
  };
889
- const RELEASE_SCHEMA = yup.object().shape({
890
- name: yup.string().trim().required(),
891
- scheduledAt: yup.string().nullable(),
892
- isScheduled: yup.boolean().optional(),
893
- time: yup.string().when("isScheduled", {
894
- is: true,
895
- then: yup.string().trim().required(),
896
- otherwise: yup.string().nullable()
897
- }),
898
- timezone: yup.string().when("isScheduled", {
899
- is: true,
900
- then: yup.string().required().nullable(),
901
- otherwise: yup.string().nullable()
902
- }),
903
- date: yup.string().when("isScheduled", {
904
- is: true,
905
- then: yup.string().required().nullable(),
906
- otherwise: yup.string().nullable()
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()
907
1343
  })
908
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()
1350
+ }).required().noUnknown();
909
1351
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
1352
+ const validatefindByDocumentAttachedParams = validateYupSchema(
1353
+ FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA
1354
+ );
910
1355
  const releaseController = {
911
- async findMany(ctx) {
912
- 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({
913
1363
  ability: ctx.state.userAbility,
914
1364
  model: RELEASE_MODEL_UID
915
1365
  });
916
1366
  await permissionsManager.validateQuery(ctx.query);
917
1367
  const releaseService = getService("release", { strapi });
918
- const isFindManyForContentTypeEntry = Boolean(ctx.query?.contentTypeUid && ctx.query?.entryId);
919
- if (isFindManyForContentTypeEntry) {
920
- const query = await permissionsManager.sanitizeQuery(ctx.query);
921
- const contentTypeUid = query.contentTypeUid;
922
- const entryId = query.entryId;
923
- const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
924
- const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
925
- ctx.body = { data };
926
- } else {
927
- const query = await permissionsManager.sanitizeQuery(ctx.query);
928
- const { results, pagination } = await releaseService.findPage(query);
929
- const data = results.map((release2) => {
930
- const { actions, ...releaseData } = release2;
931
- return {
932
- ...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: {
933
1391
  actions: {
934
- meta: {
935
- count: actions.count
1392
+ fields: ["type"],
1393
+ filters: {
1394
+ contentType,
1395
+ entryDocumentId: entryDocumentId ?? null,
1396
+ locale: locale ?? null
936
1397
  }
937
1398
  }
938
- };
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
+ }
939
1427
  });
940
- ctx.body = { data, meta: { pagination } };
1428
+ ctx.body = { data: releases };
941
1429
  }
942
1430
  },
1431
+ async findPage(ctx) {
1432
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1433
+ ability: ctx.state.userAbility,
1434
+ model: RELEASE_MODEL_UID
1435
+ });
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
1454
+ }
1455
+ });
1456
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
1457
+ },
943
1458
  async findOne(ctx) {
944
1459
  const id = ctx.params.id;
945
1460
  const releaseService = getService("release", { strapi });
1461
+ const releaseActionService = getService("release-action", { strapi });
946
1462
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
947
1463
  if (!release2) {
948
1464
  throw new errors.NotFoundError(`Release not found for id: ${id}`);
949
1465
  }
950
- const count = await releaseService.countActions({
1466
+ const count = await releaseActionService.countActions({
951
1467
  filters: {
952
1468
  release: id
953
1469
  }
954
1470
  });
955
1471
  const sanitizedRelease = {
956
1472
  ...release2,
957
- createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1473
+ createdBy: release2.createdBy ? strapi.service("admin::user").sanitizeUser(release2.createdBy) : null
958
1474
  };
959
1475
  const data = {
960
1476
  ...sanitizedRelease,
@@ -966,19 +1482,63 @@ const releaseController = {
966
1482
  };
967
1483
  ctx.body = { data };
968
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
+ },
969
1529
  async create(ctx) {
970
1530
  const user = ctx.state.user;
971
1531
  const releaseArgs = ctx.request.body;
972
1532
  await validateRelease(releaseArgs);
973
1533
  const releaseService = getService("release", { strapi });
974
1534
  const release2 = await releaseService.create(releaseArgs, { user });
975
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1535
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
976
1536
  ability: ctx.state.userAbility,
977
1537
  model: RELEASE_MODEL_UID
978
1538
  });
979
- ctx.body = {
1539
+ ctx.created({
980
1540
  data: await permissionsManager.sanitizeOutput(release2)
981
- };
1541
+ });
982
1542
  },
983
1543
  async update(ctx) {
984
1544
  const user = ctx.state.user;
@@ -987,7 +1547,7 @@ const releaseController = {
987
1547
  await validateRelease(releaseArgs);
988
1548
  const releaseService = getService("release", { strapi });
989
1549
  const release2 = await releaseService.update(id, releaseArgs, { user });
990
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1550
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
991
1551
  ability: ctx.state.userAbility,
992
1552
  model: RELEASE_MODEL_UID
993
1553
  });
@@ -1004,18 +1564,18 @@ const releaseController = {
1004
1564
  };
1005
1565
  },
1006
1566
  async publish(ctx) {
1007
- const user = ctx.state.user;
1008
1567
  const id = ctx.params.id;
1009
1568
  const releaseService = getService("release", { strapi });
1010
- const release2 = await releaseService.publish(id, { user });
1569
+ const releaseActionService = getService("release-action", { strapi });
1570
+ const release2 = await releaseService.publish(id);
1011
1571
  const [countPublishActions, countUnpublishActions] = await Promise.all([
1012
- releaseService.countActions({
1572
+ releaseActionService.countActions({
1013
1573
  filters: {
1014
1574
  release: id,
1015
1575
  type: "publish"
1016
1576
  }
1017
1577
  }),
1018
- releaseService.countActions({
1578
+ releaseActionService.countActions({
1019
1579
  filters: {
1020
1580
  release: id,
1021
1581
  type: "unpublish"
@@ -1033,57 +1593,106 @@ const releaseController = {
1033
1593
  }
1034
1594
  };
1035
1595
  const RELEASE_ACTION_SCHEMA = yup$1.object().shape({
1036
- entry: yup$1.object().shape({
1037
- id: yup$1.strapiID().required(),
1038
- contentType: yup$1.string().required()
1039
- }).required(),
1596
+ contentType: yup$1.string().required(),
1597
+ entryDocumentId: yup$1.strapiID(),
1598
+ locale: yup$1.string(),
1040
1599
  type: yup$1.string().oneOf(["publish", "unpublish"]).required()
1041
1600
  });
1042
1601
  const RELEASE_ACTION_UPDATE_SCHEMA = yup$1.object().shape({
1043
1602
  type: yup$1.string().oneOf(["publish", "unpublish"]).required()
1044
1603
  });
1604
+ const FIND_MANY_ACTIONS_PARAMS = yup$1.object().shape({
1605
+ groupBy: yup$1.string().oneOf(["action", "contentType", "locale"])
1606
+ });
1045
1607
  const validateReleaseAction = validateYupSchema(RELEASE_ACTION_SCHEMA);
1046
1608
  const validateReleaseActionUpdateSchema = validateYupSchema(RELEASE_ACTION_UPDATE_SCHEMA);
1609
+ const validateFindManyActionsParams = validateYupSchema(FIND_MANY_ACTIONS_PARAMS);
1047
1610
  const releaseActionController = {
1048
1611
  async create(ctx) {
1049
1612
  const releaseId = ctx.params.releaseId;
1050
1613
  const releaseActionArgs = ctx.request.body;
1051
1614
  await validateReleaseAction(releaseActionArgs);
1052
- const releaseService = getService("release", { strapi });
1053
- const releaseAction2 = await releaseService.createAction(releaseId, releaseActionArgs);
1054
- ctx.body = {
1615
+ const releaseActionService = getService("release-action", { strapi });
1616
+ const releaseAction2 = await releaseActionService.create(releaseId, releaseActionArgs);
1617
+ ctx.created({
1055
1618
  data: releaseAction2
1056
- };
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
+ });
1057
1658
  },
1058
1659
  async findMany(ctx) {
1059
1660
  const releaseId = ctx.params.releaseId;
1060
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1661
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1061
1662
  ability: ctx.state.userAbility,
1062
1663
  model: RELEASE_ACTION_MODEL_UID
1063
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;
1064
1673
  const query = await permissionsManager.sanitizeQuery(ctx.query);
1065
- const releaseService = getService("release", { strapi });
1066
- const { results, pagination } = await releaseService.findActions(releaseId, {
1067
- sort: query.groupBy === "action" ? "type" : query.groupBy,
1674
+ const releaseActionService = getService("release-action", { strapi });
1675
+ const { results, pagination } = await releaseActionService.findPage(releaseId, {
1068
1676
  ...query
1069
1677
  });
1070
1678
  const contentTypeOutputSanitizers = results.reduce((acc, action) => {
1071
1679
  if (acc[action.contentType]) {
1072
1680
  return acc;
1073
1681
  }
1074
- const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
1682
+ const contentTypePermissionsManager = strapi.service("admin::permission").createPermissionsManager({
1075
1683
  ability: ctx.state.userAbility,
1076
1684
  model: action.contentType
1077
1685
  });
1078
1686
  acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
1079
1687
  return acc;
1080
1688
  }, {});
1081
- const sanitizedResults = await mapAsync(results, async (action) => ({
1689
+ const sanitizedResults = await async.map(results, async (action) => ({
1082
1690
  ...action,
1083
- entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1691
+ entry: action.entry ? await contentTypeOutputSanitizers[action.contentType](action.entry) : {}
1084
1692
  }));
1085
- const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
1086
- 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 });
1087
1696
  const components = await releaseService.getAllComponents();
1088
1697
  ctx.body = {
1089
1698
  data: groupedData,
@@ -1099,8 +1708,8 @@ const releaseActionController = {
1099
1708
  const releaseId = ctx.params.releaseId;
1100
1709
  const releaseActionUpdateArgs = ctx.request.body;
1101
1710
  await validateReleaseActionUpdateSchema(releaseActionUpdateArgs);
1102
- const releaseService = getService("release", { strapi });
1103
- const updatedAction = await releaseService.updateAction(
1711
+ const releaseActionService = getService("release-action", { strapi });
1712
+ const updatedAction = await releaseActionService.update(
1104
1713
  actionId,
1105
1714
  releaseId,
1106
1715
  releaseActionUpdateArgs
@@ -1112,17 +1721,71 @@ const releaseActionController = {
1112
1721
  async delete(ctx) {
1113
1722
  const actionId = ctx.params.actionId;
1114
1723
  const releaseId = ctx.params.releaseId;
1115
- const releaseService = getService("release", { strapi });
1116
- const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
1724
+ const releaseActionService = getService("release-action", { strapi });
1725
+ const deletedReleaseAction = await releaseActionService.delete(actionId, releaseId);
1117
1726
  ctx.body = {
1118
1727
  data: deletedReleaseAction
1119
1728
  };
1120
1729
  }
1121
1730
  };
1122
- 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
+ };
1123
1754
  const release = {
1124
1755
  type: "admin",
1125
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
+ },
1126
1789
  {
1127
1790
  method: "POST",
1128
1791
  path: "/",
@@ -1142,7 +1805,7 @@ const release = {
1142
1805
  {
1143
1806
  method: "GET",
1144
1807
  path: "/",
1145
- handler: "release.findMany",
1808
+ handler: "release.findPage",
1146
1809
  config: {
1147
1810
  policies: [
1148
1811
  "admin::isAuthenticatedAdmin",
@@ -1240,6 +1903,22 @@ const releaseAction = {
1240
1903
  ]
1241
1904
  }
1242
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
+ },
1243
1922
  {
1244
1923
  method: "GET",
1245
1924
  path: "/:releaseId/actions",
@@ -1290,13 +1969,50 @@ const releaseAction = {
1290
1969
  }
1291
1970
  ]
1292
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
+ };
1293
2009
  const routes = {
2010
+ settings,
1294
2011
  release,
1295
2012
  "release-action": releaseAction
1296
2013
  };
1297
- const { features } = require("@strapi/strapi/dist/utils/ee");
1298
2014
  const getPlugin = () => {
1299
- if (features.isEnabled("cms-content-releases")) {
2015
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
1300
2016
  return {
1301
2017
  register,
1302
2018
  bootstrap,
@@ -1308,6 +2024,9 @@ const getPlugin = () => {
1308
2024
  };
1309
2025
  }
1310
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
1311
2030
  contentTypes
1312
2031
  };
1313
2032
  };